wasm-bindgen Rust: WebAssembly, JavaScript e Browser em 2026

Guia completo de wasm-bindgen em Rust: WebAssembly no browser, integração com JavaScript, web-sys, js-sys, wasm-pack, TypeScript, performance e carreira frontend/backend.

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.

wasm-bindgen Rust em 2026: onde ele encaixa

Quem busca wasm-bindgen Rust normalmente quer responder uma pergunta prática: como levar código Rust para uma aplicação web sem reescrever todo o frontend? A resposta curta é que wasm-bindgen funciona melhor como camada de interoperabilidade. Você mantém React, Vue, Svelte, JavaScript ou TypeScript onde eles são fortes, e move para Rust as partes que precisam de performance previsível, segurança de memória, processamento local ou reutilização de uma biblioteca existente.

Os casos mais comuns são filtros de imagem, compressão, criptografia, simulação, parsers, editores, visualização de dados, plugins e ferramentas que rodam no browser. Para uma visão ampla do tema, veja também Rust e WebAssembly em 2026 e o tutorial passo a passo de WebAssembly com Rust. Se o foco for aplicação web de alta performance, o guia Rust e WebAssembly: apps web de alta performance complementa esta referência com exemplos de produto.

Para carreira, o sinal forte não é “usar WASM em tudo”. O sinal forte é saber explicar quando Rust no browser vale a complexidade: gargalos reais, código compartilhado, fronteira clara com JavaScript, testes, bundle size, observabilidade e fallback. Combine wasm-bindgen com Serde para contratos de dados, Criterion para benchmark, Cargo para empacotamento e Clippy para manter o módulo confiável. Depois conecte isso a um projeto de portfólio e acompanhe as vagas Rust e empresas que usam Rust em áreas como segurança, fintech, ferramentas para devs, dados e edge computing.

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: