JSPI Expliqué : Un Meilleur Pont Entre JavaScript et Wasm
JSPI relie JavaScript et WebAssembly pour que Wasm synchrone appelle des API Promise comme fetch, avec Suspending, promising et l état des navigateurs.
JavaScript Promise Integration (JSPI) permet à un module WebAssembly d’appeler un import JavaScript retournant une Promise comme s’il s’agissait d’une fonction synchrone : le module se suspend lorsque l’import retourne une Promise et reprend son exécution avec la valeur résolue, sans gestion manuelle des callbacks. Cette seule capacité comble un fossé persistant — le code Wasm synchrone compilé depuis C, C++ ou Rust ne pouvait pas utiliser await avec une API navigateur asynchrone comme fetch ou IndexedDB sans outillage lourd. Cet article couvre le problème que JSPI résout, l’API actuelle à deux fonctions, un exemple fonctionnel avec fetch, et son état de déploiement en 2026. Un avertissement préalable : la plupart des tutoriels JSPI présentent encore une API basée sur l’objet Suspender qui a été supprimée — tout ce qui suit utilise la surface actuelle.
Points Clés
- L’API publique de JSPI se compose exactement de deux éléments :
new WebAssembly.Suspending(fn)marque un import retournant une Promise, etWebAssembly.promising(exportFn)encapsule une fonction Wasm exportée pour qu’elle retourne une Promise. - Détectez la fonctionnalité avec
'Suspending' in WebAssembly— jamais'Suspender' in WebAssembly, qui vérifie la présence de l’ancienne API supprimée avant 2024. - À mi-2026, JSPI est une proposition en phase 4 (effectivement standardisée) disponible dans Chrome 137+, dans Safari 27 beta, et dans Firefox en mode Nightly uniquement derrière une préférence, avec une activation par défaut prévue pour Firefox 153.
- Si la Promise importée est rejetée, JSPI lève une exception dans le calcul suspendu plutôt que de retourner une valeur d’erreur à Wasm.
- Contrairement à Asyncify de Binaryen, JSPI utilise la commutation de pile native du moteur, de sorte que le binaire conserve son code synchrone linéaire sans surcharge d’instrumentation.
Le pont douloureux : Wasm synchrone face au web asynchrone
Le désaccord fondamental est d’ordre architectural. WebAssembly compilé depuis C, C++ ou Rust repose sur des appels bloquants — une fonction en appelle une autre, attend la valeur de retour, puis continue. La plateforme web est à l’opposé : fetch, IndexedDB et la plupart des API navigateur modernes retournent des Promises qui se résolvent ultérieurement, pilotées par la boucle d’événements. Lorsque Wasm appelle une fonction JavaScript qui renvoie une Promise, le module n’a aucun moyen natif de se mettre en pause, d’attendre la résolution et de reprendre là où il s’était arrêté.
Avant JSPI, la solution de contournement standard était Asyncify de Binaryen, une transformation globale du programme qui réécrit le binaire Wasm pour qu’il puisse dérouler sa propre pile dans la mémoire linéaire et la rembobiner ultérieurement. Cela fonctionne, mais le coût est réel : la transformation gonfle la taille du binaire et ajoute une surcharge par appel aux fonctions instrumentées. Pour une routine de calcul à haut débit qui n’a qu’occasionnellement besoin de fetch une configuration ou de lire depuis IndexedDB en cours de calcul, payer ce tribut sur l’ensemble du module est un mauvais compromis.
Ce que fait JavaScript Promise Integration
Discover how at OpenReplay.com.
JavaScript Promise Integration fait le pont entre WebAssembly synchrone et les API Web asynchrones en transformant un appel Wasm synchrone en appel asynchrone : il suspend le module lorsqu’un import portant une Promise est appelé et le reprend lorsque la Promise est résolue. Il permet à l’application WebAssembly d’invoquer des imports dits « porteurs de Promise » et d’accéder à la valeur de la Promise, sans avoir à gérer explicitement les callbacks asynchrones normalement associés aux Promises.
Il est important de noter qu’il ne s’agit pas d’un changement de langage. La proposition n’apporte aucune modification au langage JavaScript ni au langage WebAssembly. Aucune nouvelle instruction ou nouveau type WebAssembly n’est spécifié. Sémantiquement, tous les changements décrits se situent à la frontière entre WebAssembly et JavaScript. Ce cadrage à la frontière est déterminant pour la conception de l’API et pour la portée de la suspension.
L’API en deux éléments et un exemple fonctionnel avec fetch
Toute la surface publique de JSPI se compose de deux éléments. Il y a deux éléments dans l’API JSPI : le constructeur WebAssembly.Suspending et la fonction WebAssembly.promising. new WebAssembly.Suspending(fn) marque un import retournant une Promise ; la fonction WebAssembly.promising est utilisée pour encapsuler une fonction WebAssembly exportée en une fonction qui retourne une Promise. Notez la casse — Suspending est un constructeur (avec majuscule), promising est une fonction (en minuscule).
Voici la forme canonique adaptée de l’exemple de la spécification : un import basé sur fetch encapsulé avec Suspending, un export encapsulé avec promising, et la Promise résultante attendue depuis JavaScript.
// Un import asynchrone qui retourne une Promise se résolvant en un nombre.
const computeDelta = () =>
fetch('https://example.com/data.txt')
.then(res => res.text())
.then(txt => parseFloat(txt));
const importObject = {
js: {
// Marquer l'import retournant une Promise comme suspendant.
compute_delta: new WebAssembly.Suspending(computeDelta),
},
};
const { instance } = await WebAssembly.instantiateStreaming(
fetch('module.wasm'),
importObject,
);
// Encapsuler l'export pour que son appel retourne une Promise.
const updateState = WebAssembly.promising(instance.exports.update_state);
const result = await updateState(); // se suspend dans Wasm sur compute_delta, reprend avec la valeur
À l’intérieur de update_state, le code Wasm appelle compute_delta avec une signature d’appel synchrone ordinaire. Lorsque cet import retourne une Promise, le module se suspend ; lorsque la Promise se résout, la valeur résolue devient la valeur de retour de l’import et l’exécution continue.
Comportements à connaître
Trois détails distinguent JSPI en pratique du modèle mental naïf.
La suspension est délimitée par la frontière JS/Wasm. L’import Suspending et l’export promising forment une paire — l’appel le plus imbriqué vers un export encapsulé détermine le point de coupure pour ce qui se suspend. Seuls les calculs WebAssembly peuvent être suspendus via JSPI ; cela est imposé en exigeant que seules des frames WebAssembly soient actives entre l’appel à une fonction promising et tout appel à un import encapsulé Suspending.
La suspension n’a lieu que si une Promise est effectivement retournée. Plutôt que de toujours se suspendre lors de l’appel d’une fonction JavaScript depuis un import suspendant, la suspension n’intervient que lorsque la fonction JavaScript retourne réellement une Promise. Une valeur de retour ordinaire passe directement sans passer par la boucle d’événements.
Une Promise rejetée lève une exception dans Wasm. Si la Promise est rejetée, au lieu de reprendre l’exécution du module WebAssembly avec la valeur, une exception est propagée dans le calcul suspendu. En pratique, le rejet est généralement géré côté JavaScript, car un langage comme Rust ne peut souvent pas agir directement sur cette exception — le projet wasm-bindgen a discuté de l’ajout d’un type explicite indiquant une erreur, une discussion ouverte plutôt qu’une API stabilisée.
État des navigateurs et des chaînes d’outils (2026)
JSPI a atteint la phase 4 du processus W3C WebAssembly — il est en phase 4 au sein du W3C WebAssembly WG, ce qui signifie que la spécification a été votée par le W3C Wasm CG — elle est effectivement standardisée. Cette spécification a été standardisée par le W3C WebAssembly CG en avril 2025.
| Environnement | État (mi-2026) |
|---|---|
| Chrome / Edge | Disponible en version stable depuis Chrome 137 (mai 2025) |
| Safari | Disponible dans Safari 27 beta |
| Firefox | Nightly uniquement derrière une préférence ; activation par défaut prévue pour Firefox 153 |
| Node.js | Derrière --experimental-wasm-jspi |
Pour Firefox, référez-vous au statut officiel de Mozilla plutôt qu’au chiffre « Firefox 139 » qui circule : selon l’Intent to Ship (10 juin 2026), cette fonctionnalité a été développée et déployée derrière une préférence, activée uniquement en Nightly depuis Fx152. Mozilla prévoit d’activer WebAssembly JS-Promise-Integration (JSPI) par défaut sur toutes les plateformes à partir de Firefox 153. Au moment de la rédaction, caniuse indique toujours que la version stable de Firefox ne l’active pas par défaut, vérifiez donc avant de vous y fier.
Du côté des chaînes d’outils, la plupart des projets C/C++ ne nécessitent aucune modification du code source. Si vous utilisez Emscripten, l’adoption de la nouvelle API n’implique généralement aucune modification de votre code. Vous devez utiliser une version d’Emscripten au moins égale à 3.1.61. Détectez la prise en charge proprement :
if ('Suspending' in WebAssembly) {
// JSPI est disponible — configurer Suspending / promising
} else {
// repli vers un module compilé avec Asyncify
}
Vérifiez WebAssembly.Suspending, et non Suspender : l’ancienne API continuera de fonctionner au moins jusqu’au 29 octobre 2024 (Chrome M128). Après cette date, nous prévoyons de supprimer l’ancienne API. Notez qu’Emscripten lui-même ne prendra plus en charge l’ancienne API à partir de la version 3.1.61. Une ancienne API basée sur l’objet Suspender existait et a été supprimée — si un tutoriel présente WebAssembly.Suspender ou new WebAssembly.Function(...) avec returnPromiseOnSuspend, il est obsolète.
JSPI vs Asyncify, en bref
La différence décisive réside dans l’emplacement de la logique de suspension. Asyncify la place dans votre binaire ; JSPI la place dans le moteur. Étant donné que les mécanismes utilisés lors de la suspension et de la reprise des modules WebAssembly sont essentiellement à temps constant, nous n’anticipons pas de coûts élevés liés à l’utilisation de JSPI — notamment par rapport aux autres approches basées sur des transformations. Cela se traduit par une sortie plus légère et une surcharge par appel réduite, avec une commutation de pile native au lieu d’une réécriture globale du programme. L’implémentation actuelle alloue des piles de taille fixe par calcul suspendu ; des piles extensibles (segmentées) sont prévues pour prendre en charge un grand nombre de coroutines, mais ne sont pas encore disponibles.
Si vous compilez vers Wasm et que vous vous êtes heurté à la limite où du code synchrone a besoin d’une API Web asynchrone, JSPI est la réponse actuelle : encapsulez l’import dans WebAssembly.Suspending, encapsulez l’export dans WebAssembly.promising, protégez avec 'Suspending' in WebAssembly, et conservez Asyncify uniquement comme solution de repli pour les moteurs qui ne l’ont pas encore déployé.
FAQ
Quelle est la différence entre JSPI et Asyncify ?
Asyncify est une transformation globale de programme par Binaryen qui réécrit l'intégralité du binaire Wasm pour dérouler et rembobiner sa propre pile dans la mémoire linéaire, gonflant la taille du binaire et ajoutant une surcharge par appel aux fonctions instrumentées. JSPI déplace cette logique dans le moteur en utilisant la commutation de pile native, de sorte que le module conserve un code synchrone linéaire sans instrumentation. V8 décrit les mécanismes de suspension et de reprise de JSPI comme essentiellement à temps constant, tandis qu'Asyncify pénalise l'ensemble du module.
Dois-je modifier mon code source C ou C++ Emscripten pour utiliser JSPI ?
Non. Emscripten génère automatiquement l'API JSPI actuelle à partir de la version 3.1.61, de sorte que la plupart des projets C et C++ n'ont pas besoin de modifications du code source pour passer de l'ancienne API basée sur l'objet Suspender à la nouvelle surface Suspending et promising. Il vous suffit de compiler avec Emscripten 3.1.61 ou une version ultérieure ; l'API antérieure à 2024 a été abandonnée par Emscripten à cette même version, de sorte que les chaînes d'outils plus anciennes génèrent encore la surface supprimée.
Que se passe-t-il si la Promise importée est rejetée ?
Une Promise rejetée ne retourne pas une valeur d'erreur à Wasm ; à la place, JSPI propage une exception dans le calcul suspendu. En pratique, le rejet est généralement géré côté JavaScript, car un langage comme Rust ne peut souvent pas agir directement sur l'exception levée. La signature de l'import Wasm peut indiquer un simple entier même si celui-ci représente une Promise, et wasm-bindgen dispose d'une discussion ouverte sur l'ajout d'un type explicite indiquant une erreur, plutôt qu'une API stabilisée.
L'appel d'un import JSPI suspend-il toujours le module ?
Non. JSPI ne se suspend que lorsque l'import JavaScript retourne effectivement une Promise. Si la fonction importée retourne une valeur synchrone ordinaire, le résultat passe directement au code Wasm appelant sans suspension ni passage par la boucle d'événements. Ce comportement est défini à la frontière entre JavaScript et WebAssembly, de sorte que le même import encapsulé peut se comporter de manière synchrone ou asynchrone selon ce que la fonction sous-jacente retourne à l'exécution.
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