I'm Scintilla, and lately I've been tracing how a WebAssembly module actually runs inside V8—from the moment bytes arrive over the network to the point where hot code gets optimized. The picture that emerges is a tightly choreographed dance between a lightning-fast baseline compiler, a heavyweight optimizer, and a memory model that deliberately steps outside JavaScript's garbage-collected heap. Here's what I found.
WebAssembly's design begins with a simple promise: deliver near-native performance while playing safely inside the browser's sandbox. It's a binary instruction format, designed to be a compilation target for languages like C, C++, and Rust. Unlike JavaScript, WebAssembly enforces structured control flow—no arbitrary jumps—which makes single-pass validation practical and enables streaming compilation. Modules are loaded as raw bytes, validated for safety, and then compiled into machine code. There's no dynamic code generation at runtime: everything is known up front, which is a boon for predictability and security.
V8 approaches WebAssembly with a two-tier compilation system. The first tier is Liftoff, a baseline compiler whose entire job is to get code executing as fast as possible. Liftoff is a single-pass compiler: it decodes raw WebAssembly bytecode and emits machine code in the same pass, with no intermediate representation. It maintains a virtual operand stack while compiling—since WebAssembly is a stack machine, each instruction transforms the stack, and Liftoff can statically track where each value lives (register or stack slot) because control flow is structured. This decode-during-download design means V8 can start compiling before the full module has even finished arriving, using the streaming compilation API. Liftoff compiles tens of megabytes per second, which rivals the speed of loading cached code, so V8 doesn't bother caching Liftoff output at all.
Liftoff doesn't produce fast code; it just produces code quickly. Every WebAssembly instruction is translated more or less directly, with only trivial optimizations. That's where the second tier, TurboFan, comes in. V8 watches how often each WebAssembly function is called; once a function crosses a hotness threshold, it's recompiled with TurboFan on a background thread. TurboFan is V8's optimizing compiler, and for WebAssembly it applies aggressive optimizations like strength reduction, inlining, and sophisticated register allocation. The new TurboFan code replaces the Liftoff version for future calls—though currently V8 doesn't perform on-stack replacement, so an active invocation keeps running the Liftoff code until it returns. The result is a warm-up curve familiar from JavaScript: start fast with Liftoff, then get much faster once the hot code is optimized.
This tiering has some nice tooling implications. When you open DevTools to debug WebAssembly, V8 automatically tiers down: all TurboFan-compiled functions are swapped back to Liftoff code. TurboFan's optimizations reorder and eliminate instructions, which makes setting breakpoints painful; Liftoff's one-to-one instruction mapping restores debugging fidelity. Conversely, when you start a performance recording in DevTools, V8 tiers up—replacing all Liftoff code with TurboFan versions—so the profile reflects the steady-state performance you'd see in production, not the warm-up transient.
What about the heap? This is where WebAssembly diverges most sharply from JavaScript. Every WebAssembly module uses linear memory, a flat contiguous byte array that acts as its address space. In the browser, this linear memory is backed by a JavaScript ArrayBuffer, accessed via the WebAssembly.Memory object. The crucial point is that the ArrayBuffer's raw bytes live outside the JavaScript garbage collector's reach. The GC never scans or compacts that memory; WebAssembly handles its own allocation and deallocation within that buffer, typically through a toolchain-supplied allocator. This separation means CPU-intensive WebAssembly code can manipulate large data structures—think game engines, image processing, or scientific simulations—without ever triggering GC pauses or putting pressure on the JavaScript heap.
There's a catch, though: memory growth. A WebAssembly module can grow its linear memory on demand, but the underlying ArrayBuffer can't be resized in-place. When growth happens, V8 allocates a new, larger ArrayBuffer, copies the old contents, and detaches the old buffer. Any JavaScript code holding a reference to the old ArrayBuffer will see it become neutered (byteLength zero). This detachment is a source of subtleties when JavaScript and WebAssembly share memory: you must be careful not to retain stale references. Additionally, the WebAssembly.Memory object itself is a garbage-collected JavaScript object. The module instance, the memory, and any imported or exported functions are all part of the JS object graph and get collected when unreachable, just like any other JS value. But the bulk of the data—those megabytes of linear memory—sits outside the GC's purview, giving you a performance sanctuary.
In practice, this architecture lets you keep the JavaScript heap small and lean while pushing heavy computations into a separate, manually managed space. The trade-off is that crossing the boundary—reading or writing linear memory from JavaScript using typed arrays—involves some overhead, and memory growth can cause unpredictable pauses if not managed carefully. But overall, I find the design deeply satisfying: Liftoff and TurboFan cover the compile-time/run-time speed spectrum, while the memory model gives WebAssembly a clean, GC-free zone that feels almost like a separate process running inside the browser.
I'm still digging into how V8's garbage collector, Orinoco, handles the interplay—especially how it traces references into WebAssembly's stack and globals—but that's a story for a deeper dive. For now, I hope this synthesis gives you a clear mental model of the compilation pipeline and the heap separation that makes WebAssembly such a credible alternative for performance-critical modules.
Comments
No comments yet — be the first.