Mastering WebAssembly JSPI's New API: A Step-by-Step Guide
<h2>Introduction</h2>
<p>WebAssembly’s JavaScript Promise Integration (JSPI) API has undergone a significant revision in Chrome release M126. The new API simplifies asynchronous interop for compiled C/C++ applications by removing explicit <em>Suspender</em> objects and the <code>WebAssembly.Function</code> constructor. Instead, it leverages the JavaScript/WebAssembly boundary to automatically suspend and resume computations. This guide walks you through using the updated JSPI API with Emscripten, covering everything from setup to practical implementation.</p><figure style="margin:20px 0"><img src="https://picsum.photos/seed/849731301/800/450" alt="Mastering WebAssembly JSPI's New API: A Step-by-Step Guide" style="width:100%;height:auto;border-radius:8px" loading="lazy"><figcaption style="font-size:12px;color:#666;margin-top:5px"></figcaption></figure>
<h2>What You Need</h2>
<ul>
<li><strong>Chrome M126 or later</strong> – the only browser currently supporting the new JSPI API.</li>
<li><strong>Emscripten SDK (3.1.59 or newer)</strong> – includes JSPI support and the new API wrappers.</li>
<li><strong>Node.js (optional)</strong> – for testing outside the browser.</li>
<li><strong>A C/C++ project</strong> with synchronous APIs that you want to bridge to JavaScript promises (e.g., file I/O, network requests).</li>
<li><strong>Basic familiarity</strong> with WebAssembly, Emscripten, and JavaScript promises.</li>
</ul>
<h2>Step-by-Step Guide</h2>
<h3>Step 1: Install and Configure Emscripten</h3>
<p>Download the latest Emscripten SDK from <a href="https://emscripten.org/docs/getting_started/downloads.html" target="_blank" rel="noopener">emscripten.org</a> and activate it:</p>
<pre><code>git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh</code></pre>
<p>Verify the installation with <code>emcc --version</code>. Ensure your version is 3.1.59 or above.</p>
<h3>Step 2: Compile Your C/C++ Code with JSPI Support</h3>
<p>Add the <code>-sASYNCIFY</code> and <code>-sJSPI</code> flags to your Emscripten compile command. For example:</p>
<pre><code>emcc my_program.c -o my_program.html -sASYNCIFY -sJSPI</code></pre>
<p>This tells Emscripten to instrument your code for asyncify (necessary for JSPI) and to include the JSPI runtime. If you have functions that call JavaScript promises, mark them with <code>EM_ASYNC_JS</code>:</p>
<pre><code>EM_ASYNC_JS(int, fetch_data, (const char* url), {
let response = await fetch(UTF8ToString(url));
let text = await response.text();
return stringToUTF8(text, $0, 1024);
});</code></pre>
<h3>Step 3: Create JSPI Wrappers for Your Exported Functions</h3>
<p>The new API provides <code>WebAssembly.createJSPI({ ... })</code> to wrap exported functions. No more explicit <code>Suspender</code> objects. In your JavaScript, after loading the module:</p>
<pre><code>const importObject = {
"env": {
// your imported JS functions
}
};
WebAssembly.instantiateStreaming(fetch('my_program.wasm'), importObject)
.then(obj => {
const { instance } = obj;
// Wrap the exported function that may suspend
const wrapped = WebAssembly.createJSPI({
exports: instance.exports,
exportNames: ['my_async_export']
});
// Now 'wrapped.my_async_export' returns a Promise
wrapped.my_async_export().then(result => {
console.log('Result:', result);
});
});</code></pre>
<p>The <code>createJSPI</code> function automatically determines suspension boundaries based on the outermost WebAssembly call, so you don’t need to manage cut points manually.</p>
<h3>Step 4: Call the Wrapped Export and Handle Promises</h3>
<p>When you call the wrapped export, JSPI suspends the WebAssembly computation if the called JavaScript function returns a <code>Promise</code>. It resumes once the promise resolves. Example:</p>
<pre><code>async function run() {
const result = await wrapped.my_async_export();
console.log('Done:', result);
}
run();</code></pre>
<p>If your C function does not actually encounter a promise (e.g., it calls a synchronous JS function), JSPI <strong>does not suspend</strong> – a safe optimization that avoids unnecessary event loop trips.</p>
<h3>Step 5: Test and Debug</h3>
<p>Run your application in Chrome M126+. Open DevTools > Sources > WebAssembly and set breakpoints inside your C code. You can inspect the call stack during suspension. If something fails, check the console for JSPI-related errors (e.g., “JSPI: attempted to suspend while not in a wrapped export”). Ensure you have wrapped <strong>only</strong> the top-level exports that may suspend – wrapping internal functions can cause issues.</p>
<h2>Tips for Success</h2>
<ul>
<li><strong>Keep exports minimal</strong> – Only wrap the functions that directly or indirectly interact with asynchronous JavaScript. Unnecessary wrapping adds overhead.</li>
<li><strong>Avoid nested suspensions</strong> – JSPI does not support recursive suspension. Make sure your call graph doesn’t call a wrapped export from within another wrapped export.</li>
<li><strong>Use <code>EM_ASYNC_JS</code> carefully</strong> – Each such function creates a discontinuity that JSPI handles. For simple synchronous calls, use regular <code>EM_JS</code>.</li>
<li><strong>Test on Chrome Canary</strong> – If M126 hasn’t rolled out to your stable channel, use Canary to access the latest JSPI features.</li>
<li><strong>Monitor performance</strong> – JSPI adds overhead per suspension. Profile your app to ensure it’s acceptable for your use case.</li>
<li><strong>Refer to the official spec</strong> – The <a href="https://github.com/WebAssembly/js-promise-integration" target="_blank" rel="noopener">JSPI specification</a> details edge cases and the exact behavior of the new API.</li>
</ul>
<p>With these steps, you can leverage the simplified JSPI API to bring your synchronous C/C++ WebAssembly modules into the asynchronous world of JavaScript promises – without manual <code>Suspender</code> management. Happy coding!</p>