Moving from Asyncify to JSPI for KiCad on the web — PCBJam
October 23, 2026 · all posts
TLDR: It’s worth it, all major browsers support it now. 50% improvements in bundle size, load times and speed.
Results
All results are on an M4 Max 64 GB Macbook Pro. Sizes first, both with -O1 optimization:
| Editor wasm | Asyncify | JSPI | | --- | --- | --- | | raw | 118.6 MB | 63.3 MB | | brotli | 21.0 MB | **10.6 MB** |
Good start. The following numbers are measured on vme-wren (https://ohwr.org/projects/wren/). As you can see this board is now reasonably fast, the FPS improvements are impressive.
| vme-wren | Asyncify | JSPI | | --- | --- | --- | | cold load | 2 145 ms | **1 063 ms** | | open board | 6 347 ms | **3 317 ms** | | Board pan (FPS) | 11.9 | **18.1** |
The next board we’ve tested is a Jetson AGX Thor board:
| jetson-agx-thor | Asyncify | JSPI | | --- | --- | --- | | cold load | 2 135 ms | **1 050 ms** | | open board | 14 651 ms | **8 013 ms** | | Board pan (FPS) | 11.4 | **14.4** |
Not much to add here. JSPI is better all around.
Intro - what are these things I’m talking about and why did we need them
For the past 10~ish years since the birth of WebAssembly (WASM), a runtime for making almost any language work in the browser, there was no native way for the WASM execution to stop and wait for a result from the JS engine. Why is that even needed?

Let’s say you have a C++ application that reads files from the filesystem. When you call the `getline()` function to read a line from a file, execution stops there while the OS reads it. But in a browser it’s different: any asynchronous parts are usually called from the JavaScript world: making HTTP calls, accessing files from the file browser, the list could go on.
There’s a solution for this problem: Asyncify! How it works is actually interesting: it rewrites your code in a way that makes it possible to resume it from any “suspension point” - That means adding a ton of if statements - and it’s able to save the current state of the program before suspending - think local variables, current position. It works really well for porting CLIs but in big apps like our KiCad port it
- Adds a lot to the bundle size, even at higher optimization levels
- Because of that execution is slower
- To top that in some browsers we got over some internal WASM caching limits that degraded the performance even further
- Our app doesn’t just stop -> wait for browser -> resume execution because we do react to external events from our collaborative synchronization. That means you can have multiple suspensions at the same time. Saving and restoring these adds an extra layer on top with a lot of edge cases.
- Asyncify does not support suspending from catch arms (yes, that does happen in KiCad), we eventually got around that by patching Binaryen and then we were able to switch to native exception handling
- KiCad has its own coroutines which also had to be woven into the state saving mechanism
- Asyncify can ignore the instrumentation if a function (or a function it calls down the stack) can’t suspend. Sadly both KiCad and wxWidgets that KiCad uses have a lot of indirect calls and Asyncify just instruments everything. This made the build times and build memory uses explode (it barely fit my 64 gig machine before optimizing ASYNCIFY_REMOVE). I (okay, it was Claude too) added a lot of functions to the ASYNCIFY_REMOVE list that made things better but it was a fragile and manual process that broke with higher optimization flags for example.
- And many others I forgot. In the end this led to bugs that were very hard to diagnose, I still have dreams where I get a stack corruption just before I wake up
Side note: if my explanations about Asyncify don’t click for you read this: https://kripken.github.io/blog/wasm/2019/07/16/asyncify.html
JSPI solves this by giving a native API for suspending and resuming WASM execution built into the WASM engine.
And then we switched to JSPI
When we knew that JSPI would get into Safari this September and it was already in Firefox we decided to switch. Chrome had JSPI support for a year now. In hindsight we should have switched and said “Chrome only”, but… We didn’t, I really overestimated how hard the switch would be based on my Asyncify experience. The switch was a few days of work, in the end we could remove a lot of code, a lot of bugs / stack issues were gone, the app got faster and our WASM worker threads stopped failing every now and then.
Things that got better are:
- Perf numbers
- Bundle size
- Build times
- Build system complexity
- Build memory usage
Things that we’ve lost:
- Anything before iOS 27
In short
Just switch if you can. All the major browsers now support JSPI.