12k
All articles

JSPI Explicado: Uma Ponte Melhor Entre JavaScript e Wasm

JSPI liga JavaScript e WebAssembly para que Wasm síncrono chame APIs baseadas em Promise como fetch, com Suspending, promising e status nos navegadores.

OpenReplay Team
OpenReplay Team
JSPI Explicado: Uma Ponte Melhor Entre JavaScript e Wasm

A Integração de Promises JavaScript (JSPI) permite que um módulo WebAssembly chame uma importação JavaScript que retorna uma Promise como se fosse uma função síncrona: o módulo suspende quando a importação retorna uma Promise e retoma com o valor resolvido, sem necessidade de gerenciamento manual de callbacks. Essa única capacidade preenche uma lacuna de longa data — código Wasm síncrono compilado a partir de C, C++ ou Rust não conseguia fazer await de uma API de navegador assíncrona como fetch ou IndexedDB sem ferramentas pesadas. Este artigo aborda o problema que o JSPI resolve, a API atual de duas funções, um exemplo funcional com fetch e onde ele está disponível em 2026. Um aviso importante: a maioria dos tutoriais de JSPI ainda mostra uma API com objeto Suspender que foi removida — tudo abaixo utiliza a superfície atual.

Principais Conclusões

  • A API pública do JSPI é composta exatamente por duas partes: new WebAssembly.Suspending(fn) marca uma importação que retorna uma Promise, e WebAssembly.promising(exportFn) envolve uma função Wasm exportada para que sua chamada retorne uma Promise.
  • Faça a detecção de suporte com 'Suspending' in WebAssembly — nunca com 'Suspender' in WebAssembly, que verifica a API anterior removida antes de 2024.
  • Em meados de 2026, o JSPI é uma proposta na fase 4 (efetivamente padronizada), disponível no Chrome 137+, no Safari 27 beta e no Firefox apenas no canal Nightly com uma preferência, com ativação padrão prevista para o Firefox 153.
  • Se a Promise importada for rejeitada, o JSPI lança uma exceção na computação suspensa em vez de retornar um valor de erro ao Wasm.
  • Ao contrário do Asyncify do Binaryen, o JSPI utiliza troca de pilha nativa do motor, portanto o binário mantém seu código síncrono linear sem sobrecarga de instrumentação.

A ponte problemática: Wasm síncrono encontra a web assíncrona

A incompatibilidade é arquitetural. O WebAssembly compilado a partir de C, C++ ou Rust pressupõe chamadas bloqueantes — uma função chama outra, aguarda o valor de retorno e continua. A plataforma web é o oposto: fetch, IndexedDB e a maioria das APIs modernas de navegador retornam Promises e resolvem posteriormente, conduzidas pelo loop de eventos. Quando o Wasm chama uma função JavaScript que retorna uma Promise, o módulo não tem como pausar nativamente, aguardar a resolução e retomar de onde parou.

Antes do JSPI, a solução padrão era o Asyncify do Binaryen, uma transformação de programa completo que reescreve o binário Wasm para que ele possa desenrolar sua própria pilha na memória linear e rebobiná-la posteriormente. Funciona, mas o custo é real: a transformação infla o tamanho do binário e adiciona sobrecarga por chamada às funções instrumentadas. Para uma rotina de computação de alto desempenho que ocasionalmente precisa fazer fetch de configurações ou ler do IndexedDB durante a computação, pagar esse custo em todo o módulo é um mau negócio.

O que a Integração de Promises JavaScript faz

A Integração de Promises JavaScript conecta o WebAssembly síncrono às APIs Web assíncronas mapeando uma chamada Wasm síncrona em uma assíncrona: suspende o módulo quando uma importação com Promise é chamada e o retoma quando a Promise é resolvida. Ela permite que a aplicação WebAssembly invoque as chamadas importações com Promise e acesse o valor da Promise, sem precisar gerenciar explicitamente os callbacks assíncronos normalmente associados às Promises.

É importante ressaltar que isso não é uma mudança de linguagem. A proposta não faz alterações na linguagem JavaScript nem na linguagem WebAssembly. Não há novas instruções ou tipos WebAssembly especificados. Semanticamente, todas as mudanças descritas estão na fronteira entre WebAssembly e JavaScript. Esse enquadramento de fronteira é importante para o design da API e para como a suspensão é delimitada.

A API de duas partes e um exemplo funcional com fetch

Toda a superfície pública do JSPI é composta por dois elementos. Existem dois elementos na API do JSPI: o construtor WebAssembly.Suspending e a função WebAssembly.promising. new WebAssembly.Suspending(fn) marca uma importação que retorna uma Promise; a função WebAssembly.promising é usada para envolver uma função WebAssembly exportada em uma que retorna uma Promise. Observe a capitalização — Suspending é um construtor (com maiúscula), promising é uma função (com minúscula).

A seguir está a estrutura canônica adaptada do exemplo da especificação: uma importação baseada em fetch envolvida com Suspending, uma exportação envolvida com promising e a Promise resultante aguardada a partir do JavaScript.

// An async import that returns a Promise resolving to a number.
const computeDelta = () =>
  fetch('https://example.com/data.txt')
    .then(res => res.text())
    .then(txt => parseFloat(txt));

const importObject = {
  js: {
    // Mark the Promise-returning import as suspending.
    compute_delta: new WebAssembly.Suspending(computeDelta),
  },
};

const { instance } = await WebAssembly.instantiateStreaming(
  fetch('module.wasm'),
  importObject,
);

// Wrap the export so calling it returns a Promise.
const updateState = WebAssembly.promising(instance.exports.update_state);

const result = await updateState(); // suspends inside Wasm on compute_delta, resumes with the value

Dentro de update_state, o código Wasm chama compute_delta com uma assinatura de chamada síncrona comum. Quando essa importação retorna uma Promise, o módulo suspende; quando a Promise é resolvida, o valor resolvido torna-se o valor de retorno da importação e a execução continua.

Comportamentos importantes de conhecer

Três detalhes distinguem o JSPI na prática do modelo mental ingênuo.

A suspensão é delimitada pela fronteira JS/Wasm. A importação Suspending e a exportação promising formam um par — a chamada mais interna para uma exportação envolvida determina o ponto de corte para o que é suspenso. Apenas computações WebAssembly podem ser suspensas usando JSPI; isso é garantido exigindo que apenas frames WebAssembly estejam ativos entre a chamada a uma função promising e qualquer chamada a uma importação envolvida com Suspending.

A suspensão só ocorre se uma Promise for realmente retornada. Em vez de sempre suspender ao chamar uma função JavaScript a partir de uma importação suspending, suspendemos apenas quando a função JavaScript realmente retorna uma Promise. Um valor de retorno simples é passado diretamente sem nenhuma ida ao loop de eventos.

Uma Promise rejeitada lança uma exceção no Wasm. Se a Promise for rejeitada, em vez de retomar o módulo WebAssembly com o valor, uma exceção é propagada para a computação suspensa. Na prática, a rejeição geralmente é tratada no lado do JavaScript, pois uma linguagem como Rust frequentemente não consegue agir sobre essa exceção diretamente — o projeto wasm-bindgen discutiu a adição de um tipo explícito indicador de erro, uma discussão em aberto em vez de uma API definida.

Status em navegadores e toolchains (2026)

O JSPI atingiu a fase 4 do processo WebAssembly do W3C — está na fase 4 do W3C WebAssembly WG, o que significa que a especificação foi votada pelo W3C Wasm CG — está efetivamente padronizado. Esta especificação foi padronizada pelo W3C WebAssembly CG em abril de 2025.

AmbienteStatus (meados de 2026)
Chrome / EdgeDisponível em versão estável desde o Chrome 137 (maio de 2025)
SafariDisponível no Safari 27 beta
FirefoxApenas no canal Nightly com preferência; ativação padrão prevista para o Firefox 153
Node.jsDisponível com --experimental-wasm-jspi

Para o Firefox, utilize o status oficial da Mozilla em vez do número “Firefox 139” que circula por aí: de acordo com o Intent to Ship (10 de junho de 2026), este recurso foi desenvolvido e disponibilizado com uma preferência, ativado apenas no canal Nightly desde o Fx152. A Mozilla pretende ativar a Integração de Promises JavaScript do WebAssembly (JSPI) por padrão em todas as plataformas a partir do Firefox 153. No momento da redação deste artigo, o caniuse ainda lista o Firefox estável como não ativado por padrão, portanto confirme antes de depender disso.

Em relação às toolchains, a maioria dos projetos C/C++ não precisa de alterações no código-fonte. Se você usa Emscripten, a adoção da nova API geralmente não exigirá alterações no seu código. Você deve estar usando uma versão do Emscripten de pelo menos 3.1.61. Detecte o suporte de forma limpa:

if ('Suspending' in WebAssembly) {
  // JSPI is available — wire up Suspending / promising
} else {
  // fall back to an Asyncify-built module
}

Verifique WebAssembly.Suspending, não Suspender: a API antiga continuará operando pelo menos até 29 de outubro de 2024 (Chrome M128). Após isso, planejamos remover a API antiga. Observe que o próprio Emscripten não suportará mais a API antiga a partir da versão 3.1.61. Uma API anterior com objeto Suspender existiu e foi removida — se um tutorial mostrar WebAssembly.Suspender ou new WebAssembly.Function(...) com returnPromiseOnSuspend, ele está desatualizado.

JSPI vs Asyncify, brevemente

A diferença decisiva está em onde reside a lógica de suspensão. O Asyncify a coloca no seu binário; o JSPI a coloca no motor. Como os mecanismos utilizados ao suspender e retomar módulos WebAssembly são essencialmente de tempo constante, não antecipamos custos elevados no uso do JSPI — especialmente em comparação com outras abordagens baseadas em transformação. Isso significa saída menor e menor sobrecarga por chamada, com troca de pilha nativa em vez de uma reescrita de programa completo. A implementação atual aloca pilhas de tamanho fixo por computação suspensa; pilhas crescentes (segmentadas) estão no roadmap para suportar um grande número de corrotinas, mas ainda não foram disponibilizadas.

Se você compila para Wasm e chegou ao ponto em que código síncrono precisa de uma API Web assíncrona, o JSPI é a resposta atual: envolva a importação em WebAssembly.Suspending, envolva a exportação em WebAssembly.promising, faça a verificação com 'Suspending' in WebAssembly e mantenha o Asyncify apenas como fallback para motores que ainda não o implementaram.

Perguntas Frequentes

Qual é a diferença entre JSPI e Asyncify?

O Asyncify é uma transformação Binaryen de programa completo que reescreve todo o binário Wasm para desenrolar e rebobinar sua própria pilha na memória linear, inflando o tamanho do binário e adicionando sobrecarga por chamada às funções instrumentadas. O JSPI move essa lógica para o motor usando troca de pilha nativa, de modo que o módulo mantém código síncrono linear sem instrumentação. O V8 descreve os mecanismos de suspensão e retomada do JSPI como essencialmente de tempo constante, enquanto o Asyncify onera todo o módulo.

Preciso alterar meu código-fonte C ou C++ do Emscripten para usar o JSPI?

Não. O Emscripten emite a API atual do JSPI automaticamente a partir da versão 3.1.61, portanto a maioria dos projetos C e C++ não precisa de alterações no código-fonte para migrar da antiga API com objeto Suspender para a nova superfície com Suspending e promising. Você só precisa compilar com Emscripten 3.1.61 ou posterior; a API anterior a 2024 foi removida do Emscripten nessa mesma versão, portanto toolchains mais antigas ainda emitem a superfície removida.

O que acontece se a Promise importada for rejeitada?

Uma Promise rejeitada não retorna um valor de erro ao Wasm; em vez disso, o JSPI propaga uma exceção para a computação suspensa. Na prática, a rejeição é tipicamente tratada no lado do JavaScript, pois uma linguagem como Rust frequentemente não consegue agir sobre essa exceção lançada diretamente. A assinatura da importação Wasm pode reportar um inteiro simples mesmo que represente uma Promise, e o wasm-bindgen tem uma discussão em aberto sobre a adição de um tipo explícito indicador de erro em vez de uma API definida.

Chamar uma importação JSPI sempre suspende o módulo?

Não. O JSPI só suspende quando a importação JavaScript realmente retorna uma Promise. Se a função importada retornar um valor síncrono simples, o resultado é passado diretamente ao chamador Wasm sem suspensão e sem ida ao loop de eventos. Esse comportamento é definido na fronteira entre JavaScript e WebAssembly, portanto a mesma importação envolvida pode se comportar de forma síncrona ou assíncrona dependendo do que a função subjacente retorna em tempo de execução.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.