- O parser WASM escrito em Rust é estruturalmente rápido, mas a cópia de dados e a sobrecarga de serialização na fronteira JS-WASM se revelaram um gargalo de desempenho
- O retorno direto de objetos via
serde-wasm-bindgenfoi de 9% a 29% mais lento que a serialização em JSON, devido ao custo de conversões detalhadas entre runtimes - Ao portar todo o pipeline para TypeScript, foi possível alcançar desempenho por chamada de 2,2 a 4,6 vezes mais rápido na mesma arquitetura
- No processamento em streaming, uma melhoria de O(N²) → O(N) com cache por sentença garantiu velocidade total de processamento 2,6 a 3,3 vezes maior
- Como resultado, confirmou-se que o WASM é adequado para chamadas pouco frequentes e intensivas em computação, mas inadequado para parsing de objetos JS ou funções chamadas com muita frequência
Estrutura e limites do parser WASM em Rust
- O parser
openui-langé composto por um pipeline de 6 etapas que converte a DSL gerada por LLM em uma árvore de componentes React- Etapas:
autocloser → lexer → splitter → parser → resolver → mapper → ParseResult - Cada etapa executa tokenização, análise sintática, resolução de variáveis, transformação de AST etc.
- Etapas:
- O código Rust em si é rápido, mas os processos de cópia de strings e serialização/desserialização de JSON entre JS↔WASM ocorrem a cada chamada
- Cópia da string de entrada (JS→WASM), parsing interno em Rust, serialização do resultado em JSON, cópia do JSON (WASM→JS), desserialização em JS
- Essa sobrecarga na fronteira passou a dominar o desempenho total, e a velocidade de computação do Rust não era o gargalo
Tentativa com serde-wasm-bindgen e fracasso
- Para evitar a serialização em JSON, foi aplicado
serde-wasm-bindgen, que retorna structs Rust diretamente como objetos JS - Porém, observou-se uma lentidão de 30%
- O JS não consegue ler diretamente a memória de structs Rust, e como o layout de memória difere entre os runtimes, é necessária uma conversão campo a campo
- Já a serialização em JSON gera uma string uma única vez dentro do Rust e depois é processada em JS com
JSON.parse, que é otimizado
- Resultado do benchmark
Fixture Round-trip JSON serde-wasm-bindgen Variação simple-table 20.5µs 22.5µs -9% contact-form 61.4µs 79.4µs -29% dashboard 57.9µs 74.0µs -28%
Migração para TypeScript e ganho de desempenho
- A mesma estrutura de 6 etapas foi totalmente portada para TypeScript, removendo a fronteira WASM e executando diretamente dentro do heap do V8
- Resultado do benchmark por chamada
Fixture TypeScript WASM Ganho de velocidade simple-table 9.3µs 20.5µs 2.2x contact-form 13.4µs 61.4µs 4.6x dashboard 19.4µs 57.9µs 3.0x - Só remover o WASM já reduziu drasticamente o custo por chamada, mas a ineficiência da estrutura de streaming ainda permanecia
O problema O(N²) no parsing em streaming e a melhoria
- Quando a saída do LLM é entregue em vários chunks, surge a ineficiência O(N²) de reparsear toda a string acumulada a cada vez
- Ex.: parsear 50 vezes um documento de 1000 caracteres em blocos de 20 → total de 25.000 caracteres processados
- Como solução, foi introduzido cache incremental por sentença
- Sentenças concluídas são armazenadas em cache, e apenas a sentença em andamento é parseada novamente
- A AST em cache e a nova AST são mescladas para retornar o resultado
- Benchmark considerando o stream completo
Fixture TS ingênuo TS incremental Ganho de velocidade simple-table 69µs 77µs Nenhum contact-form 316µs 122µs 2.6x dashboard 840µs 255µs 3.3x - Quanto mais sentenças houver, maior o efeito do cache, e a vazão total melhora linearmente
Lições sobre o uso de WASM
- Casos adequados
- Tarefas intensivas em computação e com pouca interação: processamento de imagem e vídeo, criptografia, simulação física, codecs de áudio etc.
- Port de bibliotecas nativas existentes: SQLite, OpenCV, libpng etc.
- Casos inadequados
- Parsing de texto estruturado em objetos JS: o custo de serialização se torna dominante
- Funções chamadas com frequência sobre entradas curtas: o custo da fronteira é maior que o da computação
- Lições principais
- É preciso fazer profiling do gargalo antes de escolher a linguagem
- O envio direto de objetos com
serde-wasm-bindgené mais caro - Melhorias na complexidade algorítmica são mais eficazes do que trocar de linguagem
- WASM e JS não compartilham o heap, e o custo de conversão sempre existe
Resultado final: com a migração para TypeScript e o cache incremental, foi alcanhada uma melhoria de 2,2 a 4,6 vezes por chamada e de 2,6 a 3,3 vezes no stream completo
2 comentários
Será que a intenção não era meio que dar uma cutucada irônica num texto super avançado sobre otimização de performance em Rust..
Comentários do Hacker News
O verdadeiro ponto central não é TypeScript em vez de Rust, e sim a correção do algoritmo de streaming, reduzindo de O(N²) para O(N)
Só essa mudança, com cache no nível de instruções (
statement), já trouxe um ganho de 3,3xIndependentemente da escolha da linguagem, do ponto de vista do usuário, o principal fator para a melhora de latência foi isso
Dá a sensação de que o título subestimou esse ponto de engenharia tão interessante
O texto em si é interessante, mas ultimamente estou cansado de títulos feitos para gerar clique
O método mede o tempo de cada chamada e usa a mediana (
median), mas, em um ambiente de navegador com lógica de defesa contra timing attacks nos motores JS, fico em dúvida sobre a precisãoDizer “reescrevemos o código da linguagem L para M e ficou mais rápido” é um resultado esperado
Porque isso cria uma oportunidade para desfazer decisões emaranhadas e equivocadas e aplicar abordagens melhores que surgiram depois
Na verdade, mesmo se L=M, vale a mesma coisa: o ganho de desempenho vem do processo de reescrita e redesign, não da linguagem
Fui investigar mais a fundo para melhorar o desempenho da serialização de objetos na fronteira entre Rust e JS
A abordagem do serde não me pareceu boa em termos de desempenho, e resumi uma tentativa de melhorar isso no meu post de blog
Eu estava me perguntando por que o Open UI não fazia trabalho relacionado a WASM
Mas aí essa nova empresa passou a usar o nome Open UI, o que me confundiu
O Open UI W3C Community Group original é, há mais de 5 anos, o grupo que cria padrões como popover em HTML,
selectcustomizável, invoker command e accordionEles estão fazendo um trabalho realmente excelente
Eles disseram que integraram
serde-wasm-bindgenna tentativa de “pular a ida e volta em JSON”, mas no fim isso parece uma reinvenção do JSON em formato binárioHoje em dia o JSON do V8 já é muito otimizado, e implementações como simdjson conseguem processar na casa de gigabytes por segundo
Acho improvável que JSON seja o gargalo
Gostei muito do design daquele blog
A barra lateral do tipo “scrollspy”, que destaca os headings conforme a posição da rolagem, ficou especialmente boa
Pelo que o Claude me disse, parece ter sido feito com fumadocs.dev
Não ficou claro para mim qual é exatamente o propósito do parser Rust WASM
O texto não explica bem essa parte e acho que precisaria de mais detalhes
Isso parece servir para evitar vazamento de informações causado por prompt injection
O parser compila os chunks transmitidos em streaming pelo LLM para montar a UI em tempo real
Antes, o parser reiniciava do zero a cada chunk; ao mudar isso para um processamento incremental (durante a migração de Rust para TypeScript), o desempenho melhorou bastante
Fiquei na dúvida se hoje em dia o TypeScript não estaria rodando com base em Golang
Brincando, mas talvez se reescreverem de novo em Rust tenham outro ganho de desempenho de 3x /s