Mastering WebAssembly JSPI's New API: A Step-by-Step Guide

By — min read
<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&#039;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>
Tags: