The Worker Thread Performance Nightmare
V8's Liftoff compiler is garbage by design and will randomly tank your WASM performance. I learned this the hard way when trying to parallelize our WebAssembly workloads - spent three days debugging what I thought was a threading issue before discovering it was V8 being shit.
The Numbers That Made Me Want To Quit
Attio's team hit the same wall when they tried to scale WASM with worker threads. Here's the performance destruction pattern:
- Single worker: 330ms (works fine)
- Two workers: 1,200ms per worker (4x slower - what the fuck?)
- Four workers: 4,053ms per worker (12x slower - completely broken)
Yeah, you read that right. Adding more workers makes each one slower, not faster. I had to re-read their blog post three times because I thought I was misunderstanding something. Parallel processing actually becomes slower than running everything sequentially. This breaks every assumption about how computers should work.
V8's "Optimization" is Actually Anti-Optimization
The culprit is V8's Liftoff compiler, which prioritizes fast compilation over fast execution. This is documented in V8's WASM compilation pipeline and confirmed by multiple performance studies. From V8's own documentation:
"The goal of Liftoff is to reduce startup time for WebAssembly-based apps by generating code as fast as possible. Code quality is secondary"
V8's Liftoff compilation pipeline - simple and fast, but produces slow code
Translation: "We made compilation fast by making execution slow as hell." This design decision makes sense for web pages that load once, but it's completely fucked for server workloads and cloud computing that execute the same WASM module repeatedly. The WebAssembly specification assumes predictable performance, but V8's implementation breaks this assumption.
How to Tell Your App is Dying
If you're seeing this bullshit:
- WASM performance varies wildly between identical runs
- Adding worker threads makes things slower instead of faster
- Some workers finish way faster than others for no reason
- Performance gets worse under load instead of better
You're hitting the Liftoff bug. Don't waste time profiling your code - it's not your fault.
The Nuclear Option That Actually Works
Disable Liftoff entirely. Yeah, it's crude, but it works:
## Copy this exact command
node --no-wasm-tier-up --no-wasm-lazy-compilation your-app.js
Or set it programmatically:
// This fixes the performance nightmare
process.env.NODE_OPTIONS = '--no-wasm-tier-up --no-wasm-lazy-compilation';
After disabling this garbage, Attio's performance went back to normal. Their worker threads actually worked like worker threads should.
Mobile Memory Will Fuck You Over
Mobile browsers will kill your WASM app for using more than 300MB, and there's basically nothing you can do about it except design around this stupid limitation.
The Mobile Death Limit
Mobile browsers are ruthless about memory:
- Chrome on Android: Dies around 300MB, sometimes way less on shit phones. I've seen apps get killed at 180MB on some garbage Samsung device
- Safari on iOS: Even more aggressive - will murder your app for looking at it wrong. Apple's memory pressure guidelines confirm this behavior
- Memory growth: Fails randomly even when you have gigabytes of free RAM. No rhyme or reason to it
Why the "Start Small and Grow" Pattern is Garbage
The WASM memory model assumes you can start with minimal memory and grow as needed. This is complete bullshit in practice, as demonstrated by Chrome's memory allocation bugs and extensive benchmarking:
- Fragmentation: Growing memory after fragmentation uses way more RAM than allocating upfront
- Commit vs reserve: No way to know if allocated memory is actually committed
- Browser roulette: Each browser has different ways to screw you over
How to Not Get Killed by Mobile
Allocate everything upfront or prepare to get murdered:
// This will fail randomly on mobile
const shittyMemory = new WebAssembly.Memory({
initial: 10, // 640KB - seems reasonable
maximum: 1000 // 64MB - will fail to grow when you need it
});
// This actually works
const workingMemory = new WebAssembly.Memory({
initial: 500, // 32MB allocated immediately
maximum: 500 // No growth allowed - can't fail what you don't try
});
The only reliable pattern: figure out your max memory needs and allocate it all immediately. Memory growth in WASM is a lie.
Android Will Murder Your App During Task Switching
Android kills background apps that use too much memory. Since WASM can't release memory back to the system, your app becomes a prime target for execution.
From Android's own docs:
"The less memory your app consumes while in the cache, the better its chances are not to be killed"
Translation: Use too much memory and Android will kill your app when the user switches tasks. Your users will think your app is broken when it's actually just dead.
These aren't edge cases - they're the fundamental reality of running WASM in production. Every successful WASM deployment has had to solve these exact problems. The next section shows you how.