Asyncify and JSPI: Stack Switching in PHP WebAssembly
Asyncify lets synchronous C or C++ code interact with asynchronous JavaScript. Technically, it saves the entire C call stack before yielding control back to JavaScript, and then restores it when the asynchronous call is finished. This is called stack switching.
Networking support in the WebAssembly PHP build is implemented using Asyncify. When PHP makes a network request, it yields control back to JavaScript, which makes the request, and then resumes PHP when the response is ready. It works well enough that PHP build can request web APIs, install composer packages, and even connect to a MySQL server.
Asyncify crashes
Stack switching requires wrapping all C functions that may be found at a call stack at a time of making an asynchronous call. Blanket-wrapping of every single C function adds a significant overhead, which is why we maintain a list of specific function names:
Unfortunately, missing even a single item from that list results in a WebAssembly crash whenever that function is a part of the call stack when an asynchronous call is made. It looks like this:

Asyncify can auto-list all the required C functions when built without ASYNCIFY_ONLY, but that auto-detection is overeager and ends up listing about 70,000 C functions which increases the startup time to 4.5s. That's why we maintain the list manually.
If you are interested in more details, see GitHub issue 251.
Fixing Asyncify crashes
Pull Request 253 adds a fix-asyncify command that runs a specialized test suite and automatically adds any identified missing C functions to the ASYNCIFY_ONLY list.
If you run into a crash like the one above, you can fix it by:
- Identifying a PHP code path that triggers the crash – the stack trace in the terminal should help with that.
- Adding a test case that triggers a crash to
packages/php-wasm/node/src/test/php-asyncify.spec.ts - Running:
npm run fix-asyncify - Committing the test case, the updated
Dockerfile, and the rebuiltPHP.wasm
JSPI: The Modern Alternative to Asyncify
The JavaScript Promise Integration (JSPI) API handles stack switching natively in V8, eliminating the need for Asyncify's function wrapping. WordPress Playground now ships JSPI builds alongside Asyncify builds for all PHP versions (7.4–8.5).
Current status:
- The Playground CLI auto-detects JSPI support and enables it automatically — no manual flags needed
- Node.js 23+ supports JSPI natively; Node.js 22 requires the
--experimental-wasm-jspiflag (handled automatically by the CLI) - Node.js 24+ is expected to have JSPI unflagged
- Browser support varies: JSPI is available in Chrome/Chromium-based browsers behind flags
Binary Size Optimization with MAIN_MODULE=2
Both Asyncify and JSPI builds are compiled with Emscripten's MAIN_MODULE=2 flag, which performs dead code elimination on exported symbols. Only symbols that dynamic extensions actually need are exported.
Impact:
- Total binary size reduced by 122 MB (13.7%)
.wasmfiles reduced by 109 MB (16%)- JavaScript glue code reduced by 14.5 MB (63%)
This optimization applies across all PHP versions (7.4–8.5) for both Node.js and Web targets. The exported symbol list is centrally managed in the Dockerfile, with conditional exports for specific extensions (e.g., __c_longjmp for Xdebug, _wasm_recv for Memcached).