Destino Dev

Blog

WebAssembly com Rust para o navegador: compilar e usar com JS (iniciantes)

Por John Pires · 19 de março de 2026

Contexto: quando voce vai usar isso

Motivacao

WebAssembly (Wasm) e uma tecnologia que traz codigo compilado para rodar no navegador com desempenho muito superior ao JavaScript em tarefas numericas e de processamento intensivo. Para quem esta começando, o uso mais comum e descarregar do JS partes que fazem processamento de texto, tokenizacao, calculos matriciais simples ou inferencia leve — ou seja, tarefas que aparecem em pipelines de IA e pre-processamento.

Casos de uso práticos

Voce vai escolher compilar Rust para Wasm quando precisar de: performance deterministica, melhor controle de memoria e bibliotecas existentes em Rust/C que implementam algoritmos de NLP, ML ou sinais. Exemplos: uma tokenizacao customizada, um calculo de scoring de tokens, ou aceleracao de transformacoes de vetores antes de enviar para um modelo.

Explicacao visual com diagramas mentais

Como o fluxo funciona (visao geral)

Browser (JS)  <--->  WebAssembly (Wasm, compilado de Rust)
 |                                  |
DOM / UI                          algoritmos numericos
Rede / Fetch -> dados brutos       tokenizacao / scoring

Imagine o navegador como uma cozinha: o JavaScript e o cozinheiro que organiza, monta e apresenta os pratos. O WebAssembly e a faca profissional e a panela de alta pressao: quando o corte fino ou o cozimento rapido sao críticos, voce usa o Wasm (escrito em Rust) para fazer esse trabalho e volta para o JS apresentar o resultado.

Dados e fronteiras (memoria e tipos)

Wasm tem sua propria area de memoria linear. A ponte entre JS e Wasm pode usar tipos simples (inteiros, floats) e também arrays tipados (Float32Array). Quando voce envia vetores numericos do JS para o Wasm, o ideal e usar TypedArray e funções expostas por wasm-bindgen que aceitam Box<[f32]> ou converter via js_sys::Float32Array. Isso evita copias desnecessarias e garante desempenho.

Primeiro exemplo simples

O que vamos construir

Exemplo prático: um crate Rust que expõe uma função score_tokens que recebe dois vetores de floats (entrada e pesos) e calcula o produto escalar. Esse calculo e comum em inferencia leve: pontuar tokens, aplicar um bias ou calcular um neurônio simples. O objetivo e aprender o fluxo de compilacao e consumo no navegador.

Passo a passo: estrutura do projeto

Os passos basicos: criar um crate de biblioteca Rust, adicionar wasm-bindgen, compilar com wasm-pack, e consumir o pacote gerado no frontend com um bundle ou diretamente como modulo ES6. Mostrarei os arquivos essenciais.

Arquivo Cargo.toml (declara dependencia wasm-bindgen). Isso configura o crate para gerar um cdylib que o wasm-pack vai empacotar.

[package]
name = "wasm_token"
version = "0.1.0"
edition = "2018"

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

[dependencies]
wasm-bindgen = "0.2"

O que este arquivo faz: configura o crate e declara wasm-bindgen como dependencia. Depois de compilado com wasm-pack, voce tera uma pasta pkg com arquivos JS e .wasm prontos para importar.

Codigo Rust: src/lib.rs. Esta funcao recebe dois arrays de floats (Box<[f32]>) e calcula produto escalar. Usamos wasm-bindgen para expor para JS.

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn score_tokens(input: Box<[f32]>, weights: Box<[f32]>) -> f32 {
    let len = std::cmp::min(input.len(), weights.len());
    let mut sum: f32 = 0.0;
    for i in 0..len {
        sum += input[i] * weights[i];
    }
    sum
}

O que acontece quando esta funcao e chamada: o Wasm recebe duas arrays alocadas a partir do JS (wasm-bindgen cuida da conversao). Ela calcula o produto escalar e retorna um f32 simples. Esse valor pode ser usado no JS para decidir ranking, threshold ou enviar para outro passo do pipeline.

Comandos para compilar (assumindo wasm-pack instalado e Rust configurado):

wasm-pack build --target web

O comando gera um pacote em pkg que voce pode importar em um projeto frontend moderno via import. Depois, no frontend, voce cria um Float32Array e passa para a funcao exposta.

Exemplo minimal de uso no navegador (index.html + index.js). A logica JS cria buffers, chama score_tokens e imprime o resultado no console.

<!doctype html>
<html>
  <body>
    <script type='module' src='index.js'></script>
  </body>
</html>

// index.js
import init, { score_tokens } from './pkg/wasm_token.js';

async function run(){
  await init();
  const input = new Float32Array([0.2, 0.7, 0.1]);
  const weights = new Float32Array([0.5, 0.3, 0.2]);
  const result = score_tokens(input, weights);
  console.log('score', result);
}

run();

O que ocorre aqui: init inicializa o runtime Wasm gerado pelo wasm-pack. Depois chamamos score_tokens com Float32Array; wasm-bindgen converte para Box<[f32]>, o calculo roda no Wasm e retorna um f32 para o JS.

Segundo exemplo mais realista

Objetivo: tokenizacao simples e pontuacao

Agora vamos implementar algo mais diretamente util para pipelines de IA: uma tokenizacao simples em Rust que normaliza texto, separa tokens e calcula uma pontuacao (por exemplo, contagem ponderada). A ideia e fazer o pre-processamento no Wasm e retornar os tokens e scores para o JS para exibir ou enviar ao modelo.

Codigo Rust: tokenizer com contagem ponderada

Este exemplo expoe duas funcoes: tokenize(text) -> Vec<String> e token_scores(tokens, weights) -> Vec<f32>. Para interoperar com JS, usamos wasm-bindgen e retornamos um JsValue com um array de strings e um Vector de floats.

use wasm_bindgen::prelude::*;
use js_sys::Array;

#[wasm_bindgen]
pub fn tokenize(text: &str) -> Array {
    let mut arr = Array::new();
    for token in text
        .to_lowercase()
        .split(|c: char| !c.is_alphanumeric())
        .filter(|s| !s.is_empty())
    {
        arr.push(&JsValue::from_str(token));
    }
    arr
}

#[wasm_bindgen]
pub fn token_scores(tokens: Box<[JsValue]>, weights: Box<[f32]>) -> Box<[f32]> {
    let mut out = Vec::with_capacity(tokens.len());
    for (i, t) in tokens.iter().enumerate() {
        let len = t.as_string().unwrap_or_default().len() as f32;
        let w = if i < weights.len() { weights[i] } else { 1.0 };
        out.push(len * w);
    }
    out.into_boxed_slice()
}

O que faz: tokenize retorna um Array JS de tokens (strings). token_scores recebe esse array convertido em Box<[JsValue]> e um vetor de pesos, calculando uma pontuacao simples (comprimento do token vezes peso) e retorna como Box<[f32]> para o JS receber um TypedArray.

Exemplo de uso JS que chama tokenize e token_scores e renderiza um pequeno JSON no console:

import init, { tokenize, token_scores } from './pkg/wasm_token.js';

async function run(){
  await init();
  const text = 'O modelo recebe tokens e calcula scores.';
  const tokens = tokenize(text); // Array de strings do Rust
  const weights = new Float32Array([1.0, 0.5, 0.8, 1.2, 1.0, 0.9]);
  // converter tokens para um Box<[JsValue]> e passar para token_scores funciona automatico via wasm-bindgen
  const scores = token_scores(tokens, weights);
  console.log('tokens', tokens);
  console.log('scores', scores);
}

run();

Comportamento: este fluxo empurra a logica de parsing e parte do calculo para Rust/Wasm. O Javascript permanece responsavel por UI, fetch de dados e orquestracao.

Erros comuns de iniciante e como evitar

Erro: tipos incompatíveis e copias desnecessarias

Uma armadilha frequente: passar arrays JS normais (Array) para funções Wasm esperando TypedArray. Isso causa copias ou erros. Use Float32Array/Int32Array no JS e tipos correspondentes em Rust (Box<[f32]>, Box<[i32]>). Evite criar strings gigantescas e fazer parsing pesado no JS se voce planeja mover essa tarefa para Rust.

Erro: esquecimento do wasm-pack init/target

Outro erro comum e compilar sem o target correto. Para usar diretamente no navegador, rode wasm-pack build --target web. Para uso com bundlers, use "bundler" como target. Se voce importar no Node.js, use "nodejs" e tenha atencao ao runtime.

Tip: durante desenvolvimento, use um servidor local (por exemplo, serve, http-server ou o próprio dev server do seu bundler). Abrir o HTML com file:// frequentemente causa problemas de carregamento de .wasm.

Erro: esquecer de inicializar o módulo Wasm

Many packages generated by wasm-pack export an init function. Always await init() antes de chamar funções expostas, caso contrario ocorrerao erros porque o .wasm ainda nao foi carregado. Em bundlers modernos voce importa o pkg e chama await init();

Dicas práticas para debugging

  1. Habilite a ferramenta de DevTools para WebAssembly no navegador (Chrome/Firefox têm visores de memória).
  2. Use console.log apenas no JS; dentro do Rust, use console_error_panic_hook::set_once() para obter panic messages legíveis no console.
  3. Testes unitarios: escreva testes em Rust para a logica antes de compilar para Wasm. Testes reduzem tempo de iteracao.

Conclusao prática

Rust + WebAssembly e uma combinação poderosa para acelerar partes criticas de um pipeline de IA no frontend: tokenizacao, transformacoes, scoring e outras rotinas numericas. Aprenda a mover apenas a logica que precisa de performance para Wasm e manter JS como orquestrador e UI.

Resumo de passos práticos:

  • Escreva a logica em Rust e exponha funcoes com #[wasm_bindgen].
  • Compile com wasm-pack --target web para uso no navegador.
  • No frontend, importe o pacote gerado, chame await init() e passe TypedArrays (Float32Array) quando trabalhar com vetores numericos.

Se voce esta começando: comece com o exemplo de produto escalar, veja como os dados fluem e depois migre tokenizacao ou outras transformacoes. Evite mover lógicas triviais que nao beneficiam de performance. Por fim, pratique incrementalmente: compile, importe, teste e use DevTools para inspecionar o .wasm.

Implementar e consumir Wasm com Rust leva tempo para pegar o ritmo, mas as vantagens em cenarios de IA no navegador costumam compensar: mais velocidade, menor latencia e controle sobre o comportamento numerico.

Assuntos: Rust, webassembly, wasm, JavaScript, frontend

Leia também