12k
All articles

JSPI Explicado: Un Mejor Puente Entre JavaScript y Wasm

JSPI conecta JavaScript y WebAssembly para que Wasm síncrono llame APIs basadas en Promise como fetch, con Suspending, promising y estado en navegadores.

OpenReplay Team
OpenReplay Team
JSPI Explicado: Un Mejor Puente Entre JavaScript y Wasm

JavaScript Promise Integration (JSPI) permite que un módulo WebAssembly llame a una importación de JavaScript que devuelve una Promise como si fuera una función síncrona: el módulo se suspende cuando la importación devuelve una Promise y se reanuda con el valor resuelto, sin necesidad de gestionar callbacks manualmente. Esta única capacidad cierra una brecha de larga data: el Wasm síncrono compilado desde C, C++ o Rust no podía hacer await de una API asíncrona del navegador como fetch o IndexedDB sin herramientas complejas. Este artículo cubre el problema que resuelve JSPI, la API actual de dos funciones, un ejemplo funcional con fetch y el estado de disponibilidad a 2026. Una advertencia importante: la mayoría de los tutoriales de JSPI aún muestran una API basada en el objeto Suspender que ha sido eliminada — todo lo que se presenta a continuación utiliza la superficie actual.

Puntos Clave

  • La API pública de JSPI se compone exactamente de dos elementos: new WebAssembly.Suspending(fn) marca una importación que devuelve una Promise, y WebAssembly.promising(exportFn) envuelve una función Wasm exportada para que su llamada devuelva una Promise.
  • Para detectar compatibilidad, use 'Suspending' in WebAssembly — nunca 'Suspender' in WebAssembly, que verifica la API anterior a 2024 que ya fue eliminada.
  • A mediados de 2026, JSPI es una propuesta en fase 4 (efectivamente estandarizada), disponible en Chrome 137+, en Safari 27 beta, y en Firefox solo en Nightly bajo una preferencia, con activación por defecto prevista para Firefox 153.
  • Si la Promise importada es rechazada, JSPI lanza una excepción en la computación suspendida en lugar de devolver un valor de error a Wasm.
  • A diferencia de Asyncify de Binaryen, JSPI utiliza cambio de pila nativo del motor, por lo que el binario conserva su código síncrono lineal sin sobrecarga por instrumentación.

El puente problemático: Wasm síncrono se encuentra con una web asíncrona

La incompatibilidad es de naturaleza arquitectónica. WebAssembly compilado desde C, C++ o Rust asume llamadas bloqueantes: una función llama a otra, espera el valor de retorno y continúa. La plataforma web funciona de manera opuesta: fetch, IndexedDB y la mayoría de las APIs modernas del navegador devuelven Promises y se resuelven más tarde, impulsadas por el event loop. Cuando Wasm llama a una función de JavaScript que devuelve una Promise, el módulo no tiene forma nativa de pausar, esperar la resolución y reanudar donde lo dejó.

Antes de JSPI, la solución estándar era Asyncify de Binaryen, una transformación de programa completo que reescribe el binario Wasm para que pueda deshacer su propia pila en memoria lineal y rehacerla más tarde. Funciona, pero el costo es real: la transformación infla el tamaño del binario y añade sobrecarga por llamada a las funciones instrumentadas. Para una rutina de cómputo de alto rendimiento que solo ocasionalmente necesita hacer fetch de una configuración o leer de IndexedDB durante el cómputo, pagar ese costo en todo el módulo es un mal negocio.

Qué hace JavaScript Promise Integration

JavaScript Promise Integration tiende un puente entre WebAssembly síncrono y las APIs web asíncronas mapeando una llamada Wasm síncrona a una asíncrona: suspende el módulo cuando se llama a una importación que devuelve una Promise y lo reanuda cuando la Promise se resuelve. Permite que la aplicación WebAssembly invoque las denominadas importaciones que devuelven Promises y acceda al valor de la Promise, sin tener que gestionar explícitamente los callbacks asíncronos normalmente asociados con las Promises.

Es importante destacar que esto no es un cambio de lenguaje. La propuesta no realiza cambios en el lenguaje JavaScript ni en el lenguaje WebAssembly. No se especifican nuevas instrucciones ni tipos de WebAssembly. Semánticamente, todos los cambios descritos se producen en el límite entre WebAssembly y JavaScript. Este enfoque centrado en el límite es relevante tanto para el diseño de la API como para la forma en que se delimita la suspensión.

La API de dos elementos y un ejemplo funcional con fetch

Toda la superficie pública de JSPI se compone de dos elementos. Existen dos elementos en la API de JSPI: el constructor WebAssembly.Suspending y la función WebAssembly.promising. new WebAssembly.Suspending(fn) marca una importación que devuelve una Promise; la función WebAssembly.promising se utiliza para envolver una función WebAssembly exportada en una que devuelve una Promise. Nótese el uso de mayúsculas: Suspending es un constructor (con mayúscula inicial), promising es una función (en minúscula).

A continuación se presenta la estructura canónica adaptada del ejemplo de la especificación: una importación basada en fetch envuelta con Suspending, una exportación envuelta con promising, y la Promise resultante esperada desde JavaScript.

// Una importación asíncrona que devuelve una Promise que se resuelve a un número.
const computeDelta = () =>
  fetch('https://example.com/data.txt')
    .then(res => res.text())
    .then(txt => parseFloat(txt));

const importObject = {
  js: {
    // Marcar la importación que devuelve una Promise como suspending.
    compute_delta: new WebAssembly.Suspending(computeDelta),
  },
};

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

// Envolver la exportación para que su llamada devuelva una Promise.
const updateState = WebAssembly.promising(instance.exports.update_state);

const result = await updateState(); // se suspende dentro de Wasm en compute_delta, se reanuda con el valor

Dentro de update_state, el código Wasm llama a compute_delta con una firma de llamada síncrona ordinaria. Cuando esa importación devuelve una Promise, el módulo se suspende; cuando la Promise se resuelve, el valor resuelto se convierte en el valor de retorno de la importación y la ejecución continúa.

Comportamientos importantes a conocer

Tres detalles distinguen a JSPI en la práctica del modelo mental simplificado.

La suspensión está delimitada por el límite JS/Wasm. La importación Suspending y la exportación promising forman un par: la llamada más interna a una exportación envuelta determina el punto de corte para lo que se suspende. Solo las computaciones WebAssembly pueden suspenderse mediante JSPI; esto se garantiza exigiendo que solo haya marcos WebAssembly activos entre la llamada a una función promising y cualquier llamada a una importación envuelta con Suspending.

Solo se suspende si realmente se devuelve una Promise. En lugar de suspenderse siempre al llamar a una función JavaScript desde una importación suspending, solo se suspende cuando la función JavaScript realmente devuelve una Promise. Un valor de retorno simple pasa directamente sin ningún paso por el event loop.

Una Promise rechazada lanza una excepción en Wasm. Si la Promise es rechazada, en lugar de reanudar el módulo WebAssembly con el valor, se propaga una excepción en la computación suspendida. En la práctica, el rechazo suele manejarse en el lado de JavaScript, ya que un lenguaje como Rust a menudo no puede actuar directamente sobre esa excepción — el proyecto wasm-bindgen ha discutido añadir un tipo explícito que indique error, una discusión abierta y no una API consolidada.

Estado en navegadores y cadenas de herramientas (2026)

JSPI alcanzó la fase 4 del proceso WebAssembly del W3C — es fase 4 en el W3C WebAssembly WG, lo que significa que la especificación fue votada por el W3C Wasm CG — está efectivamente estandarizada. Esta especificación fue estandarizada por el W3C WebAssembly CG en abril de 2025.

EntornoEstado (mediados de 2026)
Chrome / EdgeDisponible en versión estable desde Chrome 137 (mayo de 2025)
SafariDisponible en Safari 27 beta
FirefoxSolo en Nightly bajo una preferencia; activación por defecto prevista para Firefox 153
Node.jsBajo --experimental-wasm-jspi

Para Firefox, consulte el estado oficial de Mozilla en lugar del dato de “Firefox 139” que circula por ahí: según el Intent to Ship (10 de junio de 2026), esta función ha sido desarrollada y publicada bajo una preferencia, habilitada solo en Nightly desde Fx152. Mozilla tiene previsto habilitar WebAssembly JS-Promise-Integration (JSPI) por defecto en todas las plataformas a partir de Firefox 153. En el momento de redactar este artículo, caniuse aún indica que Firefox estable no tiene esta función activada por defecto, así que verifíquelo antes de depender de ella.

En cuanto a las cadenas de herramientas, la mayoría de los proyectos en C/C++ no requieren cambios en el código fuente. Si usa Emscripten, adoptar la nueva API normalmente no implicará cambios en su código. Debe usar una versión de Emscripten al menos 3.1.61. Detecte la compatibilidad de forma limpia:

if ('Suspending' in WebAssembly) {
  // JSPI está disponible — configurar Suspending / promising
} else {
  // recurrir a un módulo compilado con Asyncify
}

Verifique WebAssembly.Suspending, no Suspender: la API antigua continuará funcionando al menos hasta el 29 de octubre de 2024 (Chrome M128). Después de eso, está prevista la eliminación de la API antigua. Cabe señalar que el propio Emscripten dejó de soportar la API antigua a partir de la versión 3.1.61. Existía una API anterior basada en el objeto Suspender que fue eliminada — si un tutorial muestra WebAssembly.Suspender o new WebAssembly.Function(...) con returnPromiseOnSuspend, está desactualizado.

JSPI vs Asyncify, brevemente

La diferencia decisiva radica en dónde reside la lógica de suspensión. Asyncify la incorpora en el binario; JSPI la incorpora en el motor. Dado que los mecanismos utilizados al suspender y reanudar módulos WebAssembly son esencialmente de tiempo constante, no se anticipan costos elevados al usar JSPI — especialmente en comparación con otros enfoques basados en transformaciones. Esto se traduce en una salida más pequeña y menor sobrecarga por llamada, con cambio de pila nativo en lugar de una reescritura de todo el programa. La implementación actual asigna pilas de tamaño fijo por computación suspendida; las pilas de tamaño variable (segmentadas) están en la hoja de ruta para soportar un gran número de corrutinas, pero aún no han sido publicadas.

Si compila a Wasm y ha llegado al punto en que el código síncrono necesita una API web asíncrona, JSPI es la solución actual: envuelva la importación en WebAssembly.Suspending, envuelva la exportación en WebAssembly.promising, proteja con 'Suspending' in WebAssembly, y mantenga Asyncify solo como alternativa para los motores que aún no lo han implementado.

Preguntas Frecuentes

¿Cuál es la diferencia entre JSPI y Asyncify?

Asyncify es una transformación de programa completo de Binaryen que reescribe todo el binario Wasm para deshacer y rehacer su propia pila en memoria lineal, inflando el tamaño del binario y añadiendo sobrecarga por llamada a las funciones instrumentadas. JSPI traslada esa lógica al motor mediante cambio de pila nativo, por lo que el módulo conserva su código síncrono lineal sin instrumentación. V8 describe los mecanismos de suspensión y reanudación de JSPI como esencialmente de tiempo constante, mientras que Asyncify penaliza a todo el módulo.

¿Necesito modificar mi código fuente en C o C++ de Emscripten para usar JSPI?

No. Emscripten genera la API actual de JSPI automáticamente a partir de la versión 3.1.61, por lo que la mayoría de los proyectos en C y C++ no necesitan cambios en el código fuente para pasar de la antigua API basada en el objeto Suspender a la nueva superficie con Suspending y promising. Solo necesita compilar con Emscripten 3.1.61 o posterior; la API anterior a 2024 fue eliminada de Emscripten en esa misma versión, por lo que las cadenas de herramientas más antiguas aún generan la superficie eliminada.

¿Qué ocurre si la Promise importada es rechazada?

Una Promise rechazada no devuelve un valor de error a Wasm; en cambio, JSPI propaga una excepción en la computación suspendida. En la práctica, el rechazo suele manejarse en el lado de JavaScript, ya que un lenguaje como Rust a menudo no puede actuar directamente sobre esa excepción lanzada. La firma de importación de Wasm puede reportar un entero simple aunque represente una Promise, y wasm-bindgen tiene una discusión abierta sobre añadir un tipo explícito que indique error, en lugar de una API consolidada.

¿Llamar a una importación JSPI siempre suspende el módulo?

No. JSPI solo se suspende cuando la importación de JavaScript realmente devuelve una Promise. Si la función importada devuelve un valor síncrono simple, el resultado pasa directamente al llamador Wasm sin suspensión ni paso por el event loop. Este comportamiento está definido en el límite entre JavaScript y WebAssembly, por lo que la misma importación envuelta puede comportarse de forma síncrona o asíncrona dependiendo de lo que devuelva la función subyacente en tiempo de ejecución.

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.