12k
All articles

JSPI erklärt: Eine bessere Brücke zwischen JavaScript und Wasm

JSPI verbindet JavaScript und WebAssembly, damit synchrones Wasm Promise-basierte APIs wie fetch aufruft, mit Suspending, promising und Browserstatus.

OpenReplay Team
OpenReplay Team
JSPI erklärt: Eine bessere Brücke zwischen JavaScript und Wasm

JavaScript Promise Integration (JSPI) ermöglicht es einem WebAssembly-Modul, einen Promise-zurückgebenden JavaScript-Import aufzurufen, als wäre er eine synchrone Funktion: Das Modul wird angehalten, wenn der Import ein Promise zurückgibt, und setzt die Ausführung mit dem aufgelösten Wert fort – ohne manuelles Callback-Management. Diese eine Fähigkeit schließt eine seit langem bestehende Lücke: Synchrones Wasm, das aus C, C++ oder Rust kompiliert wurde, konnte ohne umfangreiche Werkzeuge kein await auf eine asynchrone Browser-API wie fetch oder IndexedDB anwenden. Dieser Artikel behandelt das Problem, das JSPI löst, die aktuelle Zwei-Funktionen-API, ein funktionierendes fetch-Beispiel sowie den Auslieferungsstand ab 2026. Ein Hinweis vorab: Die meisten JSPI-Tutorials zeigen noch eine entfernte Suspender-Objekt-API – alles im Folgenden verwendet die aktuelle Oberfläche.

Wichtige Erkenntnisse

  • Die öffentliche API von JSPI besteht aus genau zwei Teilen: new WebAssembly.Suspending(fn) markiert einen Promise-zurückgebenden Import, und WebAssembly.promising(exportFn) kapselt eine exportierte Wasm-Funktion, sodass ihr Aufruf ein Promise zurückgibt.
  • Feature-Erkennung erfolgt mit 'Suspending' in WebAssembly – niemals mit 'Suspender' in WebAssembly, was die vor 2024 entfernte API prüft.
  • Ab Mitte 2026 ist JSPI ein Phase-4-Proposal (faktisch standardisiert), das in Chrome 137+ ausgeliefert wird, in Safari 27 Beta verfügbar ist und in Firefox nur im Nightly-Kanal hinter einem Pref läuft, mit geplanter Standardaktivierung ab Firefox 153.
  • Wenn das importierte Promise abgelehnt wird, wirft JSPI eine Ausnahme in die angehaltene Berechnung, anstatt einen Fehlerwert an Wasm zurückzugeben.
  • Im Gegensatz zu Binaryens Asyncify verwendet JSPI natives Engine-Stack-Switching, sodass das Binary seinen geradlinigen synchronen Code ohne Instrumentierungsaufwand beibehält.

Die schwierige Brücke: Synchrones Wasm trifft auf ein asynchrones Web

Der Kern des Problems ist architektonischer Natur. WebAssembly, das aus C, C++ oder Rust kompiliert wurde, setzt blockierende Aufrufe voraus – eine Funktion ruft eine andere auf, wartet auf den Rückgabewert und fährt fort. Die Webplattform funktioniert genau umgekehrt: fetch, IndexedDB und die meisten modernen Browser-APIs geben Promises zurück und lösen sich später auf, gesteuert durch den Event-Loop. Wenn Wasm eine JavaScript-Funktion aufruft, die ein Promise zurückgibt, hat das Modul keine native Möglichkeit, anzuhalten, auf die Auflösung zu warten und dort fortzufahren, wo es aufgehört hat.

Vor JSPI war der übliche Workaround Binaryens Asyncify, eine Ganzes-Programm-Transformation, die das Wasm-Binary so umschreibt, dass es seinen eigenen Stack in den linearen Speicher abwickeln und später wieder aufwickeln kann. Es funktioniert, aber der Preis ist real: Die Transformation bläht die Binärgröße auf und fügt instrumentierten Funktionen einen Overhead pro Aufruf hinzu. Für eine hochdurchsatzfähige Rechenroutine, die nur gelegentlich fetch für Konfigurationsdaten oder IndexedDB mitten in einer Berechnung benötigt, ist dieser Aufwand für das gesamte Modul ein schlechter Kompromiss.

Was JavaScript Promise Integration leistet

JavaScript Promise Integration überbrückt synchrones WebAssembly und asynchrone Web-APIs, indem es einen synchronen Wasm-Aufruf auf einen asynchronen abbildet: Es hält das Modul an, wenn ein Promise-tragender Import aufgerufen wird, und setzt es fort, wenn das Promise sich auflöst. Es ermöglicht der WebAssembly-Anwendung, sogenannte Promise-tragende Imports aufzurufen und auf den Wert des Promise zuzugreifen, ohne die asynchronen Callbacks, die normalerweise mit Promises verbunden sind, explizit verwalten zu müssen.

Wichtig ist: Dies ist keine Sprachänderung. Der Proposal nimmt keine Änderungen an der JavaScript-Sprache oder der WebAssembly-Sprache vor. Es werden keine neuen WebAssembly-Anweisungen oder -Typen spezifiziert. Semantisch gesehen liegen alle beschriebenen Änderungen an der Grenze zwischen WebAssembly und JavaScript. Diese Grenzbetrachtung ist entscheidend für das API-Design und dafür, wie die Aussetzung eingegrenzt wird.

Die zweiteilige API und ein funktionierendes Fetch-Beispiel

Die gesamte öffentliche Oberfläche von JSPI besteht aus zwei Elementen. Es gibt zwei Elemente in der JSPI-API: den WebAssembly.Suspending-Konstruktor und die WebAssembly.promising-Funktion. new WebAssembly.Suspending(fn) markiert einen Promise-zurückgebenden Import; die Funktion WebAssembly.promising wird verwendet, um eine exportierte WebAssembly-Funktion in eine zu kapseln, die ein Promise zurückgibt. Beachten Sie die Schreibweise – Suspending ist ein Konstruktor (großgeschrieben), promising ist eine Funktion (kleingeschrieben).

Hier ist die kanonische Form, adaptiert aus dem Beispiel der Spezifikation: ein fetch-basierter Import, der mit Suspending gekapselt ist, ein Export, der mit promising gekapselt ist, und das resultierende Promise, das von JavaScript aus abgewartet wird.

// Ein asynchroner Import, der ein Promise zurückgibt, das zu einer Zahl auflöst.
const computeDelta = () =>
  fetch('https://example.com/data.txt')
    .then(res => res.text())
    .then(txt => parseFloat(txt));

const importObject = {
  js: {
    // Den Promise-zurückgebenden Import als suspending markieren.
    compute_delta: new WebAssembly.Suspending(computeDelta),
  },
};

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

// Den Export kapseln, sodass sein Aufruf ein Promise zurückgibt.
const updateState = WebAssembly.promising(instance.exports.update_state);

const result = await updateState(); // hält innerhalb von Wasm bei compute_delta an, setzt mit dem Wert fort

Innerhalb von update_state ruft der Wasm-Code compute_delta mit einer gewöhnlichen synchronen Aufruf-Signatur auf. Wenn dieser Import ein Promise zurückgibt, wird das Modul angehalten; wenn das Promise aufgelöst wird, wird der aufgelöste Wert zum Rückgabewert des Imports und die Ausführung wird fortgesetzt.

Verhaltensweisen, die man kennen sollte

Drei Details unterscheiden JSPI in der Praxis vom naiven Denkmodell.

Die Aussetzung ist durch die JS/Wasm-Grenze begrenzt. Der Suspending-Import und der promising-Export bilden ein Paar – der innerste Aufruf in einen gekapselten Export bestimmt den Schnittpunkt dafür, was ausgesetzt wird. Nur WebAssembly-Berechnungen können mit JSPI ausgesetzt werden; dies wird dadurch erzwungen, dass nur WebAssembly-Frames aktiv sein dürfen zwischen dem Aufruf einer promising-Funktion und einem Aufruf eines Suspending-gekapselten Imports.

Es wird nur ausgesetzt, wenn tatsächlich ein Promise zurückgegeben wird. Anstatt immer auszusetzen, wenn eine JavaScript-Funktion von einem suspending Import aufgerufen wird, setzen wir nur aus, wenn die JavaScript-Funktion tatsächlich ein Promise zurückgibt. Ein einfacher Rückgabewert wird direkt durchgereicht, ohne den Event-Loop zu durchlaufen.

Ein abgelehntes Promise wirft eine Ausnahme in Wasm. Wenn das Promise abgelehnt wird, wird anstelle der Fortsetzung des WebAssembly-Moduls mit dem Wert eine Ausnahme in die angehaltene Berechnung weitergegeben. In der Praxis wird die Ablehnung üblicherweise auf der JavaScript-Seite behandelt, da eine Sprache wie Rust diese geworfene Ausnahme oft nicht direkt verarbeiten kann – das wasm-bindgen-Projekt hat diskutiert, einen expliziten fehleranzeigende Typ hinzuzufügen, eine offene Diskussion statt einer festgelegten API.

Browser- und Toolchain-Status (2026)

JSPI hat Phase 4 des W3C WebAssembly-Prozesses erreicht – es befindet sich in Phase 4 der W3C WebAssembly WG, was bedeutet, dass die Spezifikation von der W3C Wasm CG abgestimmt wurde – sie ist faktisch standardisiert. Diese Spezifikation wurde im April 2025 von der W3C WebAssembly CG standardisiert.

UmgebungStatus (Mitte 2026)
Chrome / EdgeStabil ausgeliefert seit Chrome 137 (Mai 2025)
SafariVerfügbar in Safari 27 Beta
FirefoxNur im Nightly-Kanal hinter einem Pref; Standardaktivierung für Firefox 153 geplant
Node.jsHinter --experimental-wasm-jspi

Für Firefox sollte Mozillas eigener Status herangezogen werden, anstatt der kursierenden Angabe „Firefox 139”: Gemäß dem Intent to Ship (10. Juni 2026) wurde dieses Feature entwickelt und hinter einem Pref ausgeliefert, nur im Nightly-Kanal aktiviert seit Fx152. Mozilla beabsichtigt, WebAssembly JS-Promise-Integration (JSPI) standardmäßig auf allen Plattformen ab Firefox 153 zu aktivieren. Zum Zeitpunkt der Erstellung dieses Artikels listet caniuse das stabile Firefox noch als nicht standardmäßig aktiviert, daher sollte dies vor der Nutzung überprüft werden.

Bei Toolchains benötigen die meisten C/C++-Projekte keine Quelltextänderungen. Wenn Sie Emscripten verwenden, erfordert die Nutzung der neuen API in der Regel keine Änderungen an Ihrem Code. Sie müssen eine Version von Emscripten verwenden, die mindestens 3.1.61 ist. Unterstützung sauber erkennen:

if ('Suspending' in WebAssembly) {
  // JSPI ist verfügbar — Suspending / promising verdrahten
} else {
  // Auf ein mit Asyncify erstelltes Modul zurückfallen
}

Prüfen Sie WebAssembly.Suspending, nicht Suspender: Die alte API wird mindestens bis zum 29. Oktober 2024 (Chrome M128) weiter funktionieren. Danach planen wir, die alte API zu entfernen. Beachten Sie, dass Emscripten selbst die alte API ab Version 3.1.61 nicht mehr unterstützt. Eine frühere Suspender-Objekt-API existierte und wurde entfernt – wenn ein Tutorial WebAssembly.Suspender oder new WebAssembly.Function(...) mit returnPromiseOnSuspend zeigt, ist es veraltet.

JSPI vs. Asyncify, kurz zusammengefasst

Der entscheidende Unterschied liegt darin, wo die Aussetzungslogik angesiedelt ist. Asyncify platziert sie in Ihrem Binary; JSPI platziert sie in der Engine. Da die Mechanismen beim Aussetzen und Fortsetzen von WebAssembly-Modulen im Wesentlichen konstante Zeit benötigen, erwarten wir keine hohen Kosten bei der Verwendung von JSPI – insbesondere im Vergleich zu anderen transformationsbasierten Ansätzen. Das bedeutet kleinere Ausgaben und geringeren Overhead pro Aufruf, mit nativem Stack-Switching anstelle einer Ganzes-Programm-Umschreibung. Die aktuelle Implementierung allokiert Stacks fester Größe pro ausgesetzter Berechnung; erweiterbare (segmentierte) Stacks stehen auf der Roadmap, um eine große Anzahl von Coroutinen zu unterstützen, sind aber noch nicht ausgeliefert.

Wenn Sie zu Wasm kompilieren und an den Punkt gestoßen sind, wo synchroner Code eine asynchrone Web-API benötigt, ist JSPI die aktuelle Antwort: Kapseln Sie den Import in WebAssembly.Suspending, kapseln Sie den Export in WebAssembly.promising, sichern Sie mit 'Suspending' in WebAssembly ab, und behalten Sie Asyncify nur als Fallback für Engines, die es noch nicht ausgeliefert haben.

FAQs

Was ist der Unterschied zwischen JSPI und Asyncify?

Asyncify ist eine Ganzes-Programm-Binaryen-Transformation, die das gesamte Wasm-Binary umschreibt, um seinen eigenen Stack in den linearen Speicher abzuwickeln und wieder aufzuwickeln, was die Binärgröße aufbläht und instrumentierten Funktionen einen Overhead pro Aufruf hinzufügt. JSPI verlagert diese Logik mithilfe von nativem Stack-Switching in die Engine, sodass das Modul seinen geradlinigen synchronen Code ohne Instrumentierung beibehält. V8 beschreibt JSPIs Aussetzungs- und Fortsetzungsmechanismen als im Wesentlichen konstante Zeit, während Asyncify das gesamte Modul belastet.

Muss ich meinen Emscripten-C- oder C++-Quelltext ändern, um JSPI zu verwenden?

Nein. Emscripten gibt die aktuelle JSPI-API ab Version 3.1.61 automatisch aus, sodass die meisten C- und C++-Projekte keine Quelltextänderungen benötigen, um von der alten Suspender-Objekt-API zur neuen Suspending- und promising-Oberfläche zu wechseln. Sie müssen lediglich mit Emscripten 3.1.61 oder neuer bauen; die vor 2024 entfernte API wurde in Emscripten bei derselben Version aufgegeben, sodass ältere Toolchains noch die entfernte Oberfläche ausgeben.

Was passiert, wenn das importierte Promise abgelehnt wird?

Ein abgelehntes Promise gibt keinen Fehlerwert an Wasm zurück; stattdessen propagiert JSPI eine Ausnahme in die angehaltene Berechnung. In der Praxis wird die Ablehnung typischerweise auf der JavaScript-Seite behandelt, da eine Sprache wie Rust die geworfene Ausnahme oft nicht direkt verarbeiten kann. Die Wasm-Import-Signatur kann eine einfache ganze Zahl melden, obwohl sie ein Promise repräsentiert, und wasm-bindgen führt eine offene Diskussion über das Hinzufügen eines expliziten fehleranzeigenden Typs, anstatt eine festgelegte API bereitzustellen.

Setzt ein JSPI-Import-Aufruf das Modul immer aus?

Nein. JSPI setzt nur aus, wenn der JavaScript-Import tatsächlich ein Promise zurückgibt. Wenn die importierte Funktion einen einfachen synchronen Wert zurückgibt, wird das Ergebnis direkt an den Wasm-Aufrufer weitergegeben, ohne Aussetzung und ohne den Event-Loop zu durchlaufen. Dieses Verhalten ist an der JavaScript- und WebAssembly-Grenze definiert, sodass derselbe gekapselte Import sich je nachdem, was die zugrunde liegende Funktion zur Laufzeit zurückgibt, synchron oder asynchron verhalten kann.

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.