wasm-bindgen: Rust e WebAssembly na Prática

Guia completo de wasm-bindgen para Rust. Aprenda a compilar Rust para WebAssembly, exportar funções para JavaScript, importar Web APIs, usar web-sys e js-sys, workflow com wasm-pack e criar componentes web interativos.

WebAssembly (Wasm) é um formato binário que permite executar código de alta performance diretamente no navegador (e fora dele). A crate wasm-bindgen é a ponte que conecta Rust ao mundo JavaScript, permitindo que funções Rust sejam chamadas de JS e vice-versa, com conversão automática de tipos.

Rust é uma das linguagens mais bem posicionadas para WebAssembly: sem garbage collector, sem runtime pesado, com controle fino de memória e performance próxima ao código nativo. Com wasm-bindgen, web-sys e wasm-pack, você tem um ecossistema completo para criar desde bibliotecas de processamento de dados até aplicações web interativas.

Instalação

Pré-requisitos

Instale as ferramentas necessárias:

# Target de compilação WASM
rustup target add wasm32-unknown-unknown

# wasm-pack: ferramenta de build e publicação
cargo install wasm-pack

# Opcional: servidor de desenvolvimento
cargo install miniserve

Criando um Projeto WASM

# Usando wasm-pack template
cargo init meu-projeto-wasm --lib
cd meu-projeto-wasm

Configure o Cargo.toml:

[package]
name = "meu-projeto-wasm"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = [
    "console",
    "Document",
    "Element",
    "HtmlElement",
    "HtmlCanvasElement",
    "CanvasRenderingContext2d",
    "Window",
    "Node",
    "Event",
    "MouseEvent",
    "KeyboardEvent",
    "HtmlInputElement",
    "CssStyleDeclaration",
    "DomTokenList",
] }

[profile.release]
opt-level = "s"      # Otimizar para tamanho
lto = true           # Link-Time Optimization

Uso Básico

Exportando Funções para JavaScript

// src/lib.rs
use wasm_bindgen::prelude::*;

// Exportar uma função simples
#[wasm_bindgen]
pub fn somar(a: i32, b: i32) -> i32 {
    a + b
}

// Exportar com nome diferente em JS
#[wasm_bindgen(js_name = cumprimentar)]
pub fn saudar(nome: &str) -> String {
    format!("Olá, {}! Bem-vindo ao Rust + WASM!", nome)
}

// Função que recebe e retorna tipos complexos
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> Vec<u32> {
    let mut seq = vec![0, 1];
    for i in 2..n as usize {
        let proximo = seq[i - 1] + seq[i - 2];
        seq.push(proximo);
    }
    seq
}

// Função que pode falhar
#[wasm_bindgen]
pub fn dividir(a: f64, b: f64) -> Result<f64, JsValue> {
    if b == 0.0 {
        Err(JsValue::from_str("Divisão por zero!"))
    } else {
        Ok(a / b)
    }
}

Compilando e Usando

# Compilar com wasm-pack
wasm-pack build --target web

# Isso gera a pasta pkg/ com:
# - meu_projeto_wasm_bg.wasm  (binário WASM)
# - meu_projeto_wasm.js       (glue code JS)
# - meu_projeto_wasm.d.ts     (tipos TypeScript)
# - package.json

Usando no HTML:

<!DOCTYPE html>
<html>
<head>
    <title>Rust + WASM</title>
</head>
<body>
    <h1>Rust WebAssembly</h1>
    <div id="resultado"></div>

    <script type="module">
        import init, { somar, cumprimentar, fibonacci, dividir }
            from './pkg/meu_projeto_wasm.js';

        async function main() {
            await init();

            console.log("2 + 3 =", somar(2, 3));
            console.log(cumprimentar("Maria"));
            console.log("Fibonacci:", fibonacci(10));

            try {
                console.log("10 / 3 =", dividir(10, 3));
                console.log("10 / 0 =", dividir(10, 0));
            } catch (e) {
                console.error("Erro:", e);
            }

            document.getElementById("resultado").textContent =
                cumprimentar("Mundo");
        }

        main();
    </script>
</body>
</html>

Importando Funções JavaScript

use wasm_bindgen::prelude::*;

// Importar funções globais do JS
#[wasm_bindgen]
extern "C" {
    // window.alert()
    fn alert(s: &str);

    // console.log()
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);

    #[wasm_bindgen(js_namespace = console, js_name = log)]
    fn log_u32(a: u32);

    #[wasm_bindgen(js_namespace = console, js_name = log)]
    fn log_many(a: &str, b: &str);

    // performance.now()
    #[wasm_bindgen(js_namespace = performance)]
    fn now() -> f64;
}

// Macro auxiliar para console.log
macro_rules! console_log {
    ($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}

#[wasm_bindgen]
pub fn demonstrar_imports() {
    console_log!("Olá do Rust via console.log!");
    console_log!("Performance.now(): {:.2}ms", now());
    log_many("Múltiplos", "argumentos");
}

Structs Exportadas

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct Contador {
    valor: i32,
    historico: Vec<i32>,
}

#[wasm_bindgen]
impl Contador {
    #[wasm_bindgen(constructor)]
    pub fn novo(valor_inicial: i32) -> Contador {
        Contador {
            valor: valor_inicial,
            historico: vec![valor_inicial],
        }
    }

    pub fn incrementar(&mut self) {
        self.valor += 1;
        self.historico.push(self.valor);
    }

    pub fn decrementar(&mut self) {
        self.valor -= 1;
        self.historico.push(self.valor);
    }

    pub fn valor(&self) -> i32 {
        self.valor
    }

    pub fn resetar(&mut self, valor: i32) {
        self.valor = valor;
        self.historico.clear();
        self.historico.push(valor);
    }

    pub fn historico_json(&self) -> String {
        format!("{:?}", self.historico)
    }
}

Uso em JavaScript:

import { Contador } from './pkg/meu_projeto_wasm.js';

const contador = new Contador(0);
contador.incrementar();
contador.incrementar();
contador.incrementar();
contador.decrementar();
console.log("Valor:", contador.valor());       // 2
console.log("Histórico:", contador.historico_json()); // [0, 1, 2, 3, 2]
contador.free(); // Liberar memória WASM

Recursos Avançados

web-sys: APIs do Navegador

A crate web-sys fornece bindings para todas as Web APIs:

use wasm_bindgen::prelude::*;
use web_sys::{console, Document, Element, HtmlElement, Window};

fn window() -> Window {
    web_sys::window().expect("Sem window global")
}

fn document() -> Document {
    window().document().expect("Sem document")
}

#[wasm_bindgen]
pub fn manipular_dom() -> Result<(), JsValue> {
    let document = document();

    // Criar elemento
    let div = document.create_element("div")?;
    div.set_id("meu-div");
    div.set_class_name("container ativo");
    div.set_inner_html("<h2>Criado pelo Rust!</h2><p>Manipulação de DOM via wasm-bindgen.</p>");

    // Estilizar
    let estilo = div
        .dyn_ref::<HtmlElement>()
        .unwrap()
        .style();
    estilo.set_property("background-color", "#f0f0f0")?;
    estilo.set_property("padding", "20px")?;
    estilo.set_property("border-radius", "8px")?;
    estilo.set_property("margin", "10px")?;

    // Adicionar ao body
    let body = document.body().unwrap();
    body.append_child(&div)?;

    // Buscar elementos existentes
    if let Some(titulo) = document.get_element_by_id("titulo") {
        titulo.set_text_content(Some("Título atualizado pelo Rust!"));
    }

    // Query selector
    let paragrafos = document.query_selector_all("p")?;
    console::log_1(&format!("Encontrados {} parágrafos", paragrafos.length()).into());

    Ok(())
}

Event Listeners com Closures

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{Document, Event, HtmlInputElement, MouseEvent};

#[wasm_bindgen]
pub fn configurar_eventos() -> Result<(), JsValue> {
    let document = web_sys::window().unwrap().document().unwrap();

    // Evento de clique
    let botao = document.get_element_by_id("meu-botao").unwrap();
    let callback = Closure::wrap(Box::new(move |evento: MouseEvent| {
        let x = evento.client_x();
        let y = evento.client_y();
        web_sys::console::log_1(
            &format!("Clique em ({}, {})", x, y).into(),
        );
    }) as Box<dyn FnMut(MouseEvent)>);

    botao.add_event_listener_with_callback(
        "click",
        callback.as_ref().unchecked_ref(),
    )?;
    callback.forget(); // Manter o closure vivo

    // Evento de input
    let input = document.get_element_by_id("meu-input").unwrap();
    let output = document.get_element_by_id("output").unwrap();

    let callback_input = Closure::wrap(Box::new(move |evento: Event| {
        let target = evento.target().unwrap();
        let input = target.dyn_ref::<HtmlInputElement>().unwrap();
        let valor = input.value();
        let maiusculo = valor.to_uppercase();

        if let Some(el) = web_sys::window()
            .unwrap()
            .document()
            .unwrap()
            .get_element_by_id("output")
        {
            el.set_text_content(Some(&maiusculo));
        }
    }) as Box<dyn FnMut(Event)>);

    input.add_event_listener_with_callback(
        "input",
        callback_input.as_ref().unchecked_ref(),
    )?;
    callback_input.forget();

    Ok(())
}

js-sys: Tipos JavaScript Nativos

use wasm_bindgen::prelude::*;
use js_sys::{Array, Date, JSON, Map, Object, Promise, Reflect};

#[wasm_bindgen]
pub fn demonstrar_js_sys() -> Result<JsValue, JsValue> {
    // Date
    let agora = Date::new_0();
    web_sys::console::log_1(&format!(
        "Data JS: {}/{}/{}",
        agora.get_date(),
        agora.get_month() + 1,
        agora.get_full_year()
    ).into());

    // Array
    let arr = Array::new();
    arr.push(&JsValue::from("Rust"));
    arr.push(&JsValue::from("WebAssembly"));
    arr.push(&JsValue::from("JavaScript"));
    web_sys::console::log_1(&format!("Array length: {}", arr.length()).into());

    // Map
    let mapa = Map::new();
    mapa.set(&JsValue::from("linguagem"), &JsValue::from("Rust"));
    mapa.set(&JsValue::from("versao"), &JsValue::from("1.75"));

    // Object
    let obj = Object::new();
    Reflect::set(&obj, &"nome".into(), &"wasm-bindgen".into())?;
    Reflect::set(&obj, &"versao".into(), &"0.2".into())?;

    // JSON
    let json_str = JSON::stringify(&obj)?;
    web_sys::console::log_1(&json_str);

    Ok(obj.into())
}

Processamento de Dados Pesado

Um dos melhores casos de uso para WASM: processamento intensivo de dados:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn processar_imagem(dados: &[u8], largura: u32, altura: u32) -> Vec<u8> {
    let mut resultado = dados.to_vec();

    // Converter para escala de cinza
    for pixel in resultado.chunks_exact_mut(4) {
        let r = pixel[0] as f32;
        let g = pixel[1] as f32;
        let b = pixel[2] as f32;
        let cinza = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
        pixel[0] = cinza;
        pixel[1] = cinza;
        pixel[2] = cinza;
        // pixel[3] (alpha) permanece inalterado
    }

    resultado
}

#[wasm_bindgen]
pub fn aplicar_blur(dados: &[u8], largura: u32, altura: u32, raio: u32) -> Vec<u8> {
    let mut resultado = dados.to_vec();
    let w = largura as usize;
    let h = altura as usize;
    let r = raio as usize;

    for y in r..h - r {
        for x in r..w - r {
            let mut soma_r = 0u32;
            let mut soma_g = 0u32;
            let mut soma_b = 0u32;
            let mut contagem = 0u32;

            for dy in 0..=2 * r {
                for dx in 0..=2 * r {
                    let ny = y + dy - r;
                    let nx = x + dx - r;
                    let idx = (ny * w + nx) * 4;
                    soma_r += dados[idx] as u32;
                    soma_g += dados[idx + 1] as u32;
                    soma_b += dados[idx + 2] as u32;
                    contagem += 1;
                }
            }

            let idx = (y * w + x) * 4;
            resultado[idx] = (soma_r / contagem) as u8;
            resultado[idx + 1] = (soma_g / contagem) as u8;
            resultado[idx + 2] = (soma_b / contagem) as u8;
        }
    }

    resultado
}

// Sorting eficiente em WASM
#[wasm_bindgen]
pub fn ordenar_numeros(mut numeros: Vec<f64>) -> Vec<f64> {
    numeros.sort_by(|a, b| a.partial_cmp(b).unwrap());
    numeros
}

// Busca em texto (muito mais rápido que JS para textos grandes)
#[wasm_bindgen]
pub fn contar_ocorrencias(texto: &str, padrao: &str) -> u32 {
    texto.matches(padrao).count() as u32
}

Workflow com wasm-pack

# Build para web (ES modules)
wasm-pack build --target web

# Build para bundlers (webpack, vite, etc.)
wasm-pack build --target bundler

# Build para Node.js
wasm-pack build --target nodejs

# Build com otimizações
wasm-pack build --release --target web

# Testar no navegador
wasm-pack test --chrome --headless

# Publicar no npm
wasm-pack publish

Estrutura do pkg/ gerado:

pkg/
├── meu_projeto_wasm_bg.wasm    # Binário WASM
├── meu_projeto_wasm_bg.wasm.d.ts # Tipos do WASM
├── meu_projeto_wasm.js          # Glue code JS
├── meu_projeto_wasm.d.ts        # Tipos TypeScript
├── package.json                  # Para npm
└── README.md

Boas Práticas

1. Minimize as Chamadas entre JS e WASM

use wasm_bindgen::prelude::*;

// RUIM: muitas chamadas individuais JS -> WASM
#[wasm_bindgen]
pub fn somar_um(n: f64) -> f64 {
    n + 1.0
}
// JS: for (let i = 0; i < 1000; i++) { arr[i] = somar_um(arr[i]); }

// BOM: processar em lote
#[wasm_bindgen]
pub fn somar_um_lote(dados: &[f64]) -> Vec<f64> {
    dados.iter().map(|n| n + 1.0).collect()
}
// JS: arr = somar_um_lote(arr);

2. Gerencie Memória Corretamente

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct RecursoPesado {
    dados: Vec<u8>,
}

#[wasm_bindgen]
impl RecursoPesado {
    #[wasm_bindgen(constructor)]
    pub fn novo(tamanho: usize) -> RecursoPesado {
        RecursoPesado {
            dados: vec![0; tamanho],
        }
    }

    // Método free() é gerado automaticamente pelo wasm-bindgen
    // JS deve chamar recurso.free() quando não precisar mais
}
// Uso correto em JS:
const recurso = new RecursoPesado(1024 * 1024);
// ... usar recurso ...
recurso.free(); // IMPORTANTE: liberar memória WASM

3. Use #[wasm_bindgen(start)] para Inicialização

use wasm_bindgen::prelude::*;

#[wasm_bindgen(start)]
pub fn inicializar() {
    // Configurar panic hook para mensagens de erro úteis
    #[cfg(feature = "console_error_panic_hook")]
    console_error_panic_hook::set_once();

    web_sys::console::log_1(&"Módulo WASM inicializado!".into());
}

4. Otimize o Tamanho do WASM

# Cargo.toml
[profile.release]
opt-level = "s"       # Otimizar para tamanho (ou "z" para menor possível)
lto = true            # Link-Time Optimization
codegen-units = 1     # Melhor otimização
strip = true          # Remover símbolos de debug

[dependencies]
console_error_panic_hook = { version = "0.1", optional = true }
wee_alloc = { version = "0.4", optional = true }

[features]
default = ["console_error_panic_hook"]
# Usar wasm-opt para otimização adicional (instalado com wasm-pack)
wasm-pack build --release

5. Tratamento de Erros Informativo

use wasm_bindgen::prelude::*;

// Configurar panic hook para erros legíveis no console do navegador
pub fn configurar_panic_hook() {
    #[cfg(feature = "console_error_panic_hook")]
    console_error_panic_hook::set_once();
}

#[wasm_bindgen]
pub fn operacao_que_pode_falhar(dados: &str) -> Result<String, JsValue> {
    configurar_panic_hook();

    serde_json::from_str::<serde_json::Value>(dados)
        .map(|v| v.to_string())
        .map_err(|e| JsValue::from_str(&format!("Erro de parsing: {}", e)))
}

Exemplos Práticos

Exemplo Completo: Componente Web Interativo

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{
    CanvasRenderingContext2d, Document, HtmlCanvasElement,
    HtmlElement, MouseEvent, Window,
};
use std::cell::RefCell;
use std::rc::Rc;

fn window() -> Window {
    web_sys::window().unwrap()
}

fn document() -> Document {
    window().document().unwrap()
}

#[wasm_bindgen]
pub struct App {
    canvas: HtmlCanvasElement,
    ctx: CanvasRenderingContext2d,
}

#[derive(Clone)]
struct Ponto {
    x: f64,
    y: f64,
    cor: String,
    raio: f64,
}

#[wasm_bindgen]
impl App {
    #[wasm_bindgen(constructor)]
    pub fn novo(canvas_id: &str) -> Result<App, JsValue> {
        let document = document();
        let canvas = document
            .get_element_by_id(canvas_id)
            .ok_or_else(|| JsValue::from_str("Canvas não encontrado"))?
            .dyn_into::<HtmlCanvasElement>()?;

        let ctx = canvas
            .get_context("2d")?
            .unwrap()
            .dyn_into::<CanvasRenderingContext2d>()?;

        Ok(App { canvas, ctx })
    }

    pub fn iniciar(&self) -> Result<(), JsValue> {
        let largura = self.canvas.width() as f64;
        let altura = self.canvas.height() as f64;

        // Fundo
        self.ctx.set_fill_style_str("#1a1a2e");
        self.ctx.fill_rect(0.0, 0.0, largura, altura);

        // Título
        self.ctx.set_fill_style_str("#e94560");
        self.ctx.set_font("24px Arial");
        self.ctx.set_text_align("center");
        self.ctx
            .fill_text("Clique para desenhar!", largura / 2.0, 40.0)?;

        // Configurar evento de clique
        let canvas = self.canvas.clone();
        let ctx = self
            .canvas
            .get_context("2d")
            .unwrap()
            .unwrap()
            .dyn_into::<CanvasRenderingContext2d>()
            .unwrap();

        let pontos = Rc::new(RefCell::new(Vec::<Ponto>::new()));
        let pontos_clone = pontos.clone();

        let callback = Closure::wrap(Box::new(move |evento: MouseEvent| {
            let rect = canvas.get_bounding_client_rect();
            let x = evento.client_x() as f64 - rect.left();
            let y = evento.client_y() as f64 - rect.top();

            let cores = ["#e94560", "#0f3460", "#16213e", "#533483", "#e94560"];
            let idx = pontos_clone.borrow().len() % cores.len();

            let ponto = Ponto {
                x,
                y,
                cor: cores[idx].to_string(),
                raio: 5.0 + (pontos_clone.borrow().len() as f64 % 20.0),
            };

            // Desenhar círculo
            ctx.begin_path();
            ctx.arc(ponto.x, ponto.y, ponto.raio, 0.0, std::f64::consts::PI * 2.0)
                .unwrap();
            ctx.set_fill_style_str(&ponto.cor);
            ctx.fill();

            // Conectar ao ponto anterior
            let pontos_ref = pontos_clone.borrow();
            if let Some(anterior) = pontos_ref.last() {
                ctx.begin_path();
                ctx.move_to(anterior.x, anterior.y);
                ctx.line_to(ponto.x, ponto.y);
                ctx.set_stroke_style_str(&ponto.cor);
                ctx.set_line_width(2.0);
                ctx.stroke();
            }
            drop(pontos_ref);

            pontos_clone.borrow_mut().push(ponto);

        }) as Box<dyn FnMut(MouseEvent)>);

        self.canvas.add_event_listener_with_callback(
            "click",
            callback.as_ref().unchecked_ref(),
        )?;
        callback.forget();

        Ok(())
    }

    pub fn limpar(&self) {
        let largura = self.canvas.width() as f64;
        let altura = self.canvas.height() as f64;
        self.ctx.set_fill_style_str("#1a1a2e");
        self.ctx.fill_rect(0.0, 0.0, largura, altura);
    }

    pub fn desenhar_grafico(&self, dados: &[f64]) -> Result<(), JsValue> {
        let largura = self.canvas.width() as f64;
        let altura = self.canvas.height() as f64;
        let margem = 50.0;

        // Limpar
        self.ctx.set_fill_style_str("#1a1a2e");
        self.ctx.fill_rect(0.0, 0.0, largura, altura);

        if dados.is_empty() {
            return Ok(());
        }

        let max_val = dados.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
        let min_val = dados.iter().cloned().fold(f64::INFINITY, f64::min);
        let range = if max_val == min_val { 1.0 } else { max_val - min_val };

        let area_largura = largura - 2.0 * margem;
        let area_altura = altura - 2.0 * margem;
        let passo = area_largura / (dados.len() - 1).max(1) as f64;

        // Eixos
        self.ctx.set_stroke_style_str("#555");
        self.ctx.set_line_width(1.0);
        self.ctx.begin_path();
        self.ctx.move_to(margem, margem);
        self.ctx.line_to(margem, altura - margem);
        self.ctx.line_to(largura - margem, altura - margem);
        self.ctx.stroke();

        // Linha do gráfico
        self.ctx.set_stroke_style_str("#e94560");
        self.ctx.set_line_width(2.0);
        self.ctx.begin_path();

        for (i, &valor) in dados.iter().enumerate() {
            let x = margem + i as f64 * passo;
            let y = altura - margem - ((valor - min_val) / range * area_altura);

            if i == 0 {
                self.ctx.move_to(x, y);
            } else {
                self.ctx.line_to(x, y);
            }
        }
        self.ctx.stroke();

        // Pontos
        self.ctx.set_fill_style_str("#e94560");
        for (i, &valor) in dados.iter().enumerate() {
            let x = margem + i as f64 * passo;
            let y = altura - margem - ((valor - min_val) / range * area_altura);
            self.ctx.begin_path();
            self.ctx.arc(x, y, 4.0, 0.0, std::f64::consts::PI * 2.0)?;
            self.ctx.fill();
        }

        // Labels
        self.ctx.set_fill_style_str("#aaa");
        self.ctx.set_font("12px monospace");
        self.ctx.set_text_align("right");
        self.ctx.fill_text(&format!("{:.1}", max_val), margem - 5.0, margem + 5.0)?;
        self.ctx.fill_text(
            &format!("{:.1}", min_val),
            margem - 5.0,
            altura - margem + 5.0,
        )?;

        Ok(())
    }
}

HTML para o exemplo:

<!DOCTYPE html>
<html>
<head>
    <title>Rust WASM Canvas</title>
    <style>
        body {
            background: #0f0f23;
            color: white;
            font-family: Arial, sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 20px;
        }
        canvas {
            border: 2px solid #333;
            border-radius: 8px;
            cursor: crosshair;
        }
        button {
            margin: 10px;
            padding: 10px 20px;
            background: #e94560;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
        }
        button:hover { background: #c73e55; }
    </style>
</head>
<body>
    <h1>Rust + WebAssembly Canvas</h1>
    <canvas id="canvas" width="800" height="500"></canvas>
    <div>
        <button id="btn-limpar">Limpar</button>
        <button id="btn-grafico">Gráfico Demo</button>
    </div>

    <script type="module">
        import init, { App } from './pkg/meu_projeto_wasm.js';

        async function main() {
            await init();

            const app = new App("canvas");
            app.iniciar();

            document.getElementById("btn-limpar").onclick = () => {
                app.limpar();
                app.iniciar();
            };

            document.getElementById("btn-grafico").onclick = () => {
                const dados = Array.from({length: 20}, () => Math.random() * 100);
                app.desenhar_grafico(new Float64Array(dados));
            };
        }

        main();
    </script>
</body>
</html>

Comparação com Alternativas

TecnologiaCaso de usoDestaques
wasm-bindgenInterop Rust-JSMais maduro, tipagem rica
stdwebInterop Rust-JSDescontinuado, use wasm-bindgen
YewFramework SPASimilar ao React, componentes
LeptosFramework SPASignals, SSR, muito performático
DioxusFramework multiplataformaWeb, desktop, mobile
TrunkBuild toolBundling para projetos WASM

Conclusão

A combinação de wasm-bindgen, web-sys e wasm-pack torna Rust uma opção de primeira classe para WebAssembly. Desde processamento de imagens e criptografia até jogos e visualizações de dados, o WASM com Rust entrega performance nativa no navegador com segurança de memória.

O workflow moderno com wasm-pack simplifica build, testes e publicação, gerando automaticamente tipos TypeScript e pacotes npm. A interoperabilidade bidirecional entre Rust e JavaScript, com conversão automática de tipos, torna o desenvolvimento surpreendentemente ergonômico.

Próximos passos:

  • Explore frameworks como Yew ou Leptos para SPAs completas em Rust
  • Veja criterion para benchmarkar seu código WASM
  • Confira serde para serialização de dados entre Rust e JS