Under the Hood
Wasm

Threads, SIMD, and where WASM is going

Core WebAssembly is a single-threaded, scalar stack machine, but a set of post-MVP proposals — SIMD for data parallelism, threads for true multicore execution, and a managed-heap GC on the way — are steadily turning it into as complete a compute target as native code, and this closing lesson traces each one back to a mechanism earlier lessons already built.

Threads, SIMD, and where WASM is going

Every lesson in this module so far described WASM as it shipped at launch: one stack machine, one linear memory, one thread of execution, arithmetic on one value at a time. That core was deliberately minimal — small enough to specify precisely, validate fast, and get right. But minimal isn't the end state; it's the foundation a sequence of post-launch proposals have been building on ever since. This closing lesson covers the two that matter most for raw performance — SIMD and threads — plus a shorter survey of what else is on the way, and then pulls the whole module's arc together.

SIMD: more instructions, not a different machine

Recall from the flagship lesson that every WASM instruction is a small, fixed operation: pop some typed values, do one thing, push the result. i32.add pops two 32-bit integers and pushes their sum. SIMD — single instruction, multiple data — extends that same idea with a new value type and a family of instructions that operate on several numbers packed into one value at once, rather than one number at a time.

The core addition is a 128-bit vector type, v128, that can hold, for example, four packed f32 lanes — four 32-bit floats sitting side by side in one 128-bit value. An instruction like f32x4.add pops two v128s and adds them lane-wise: lane 0 of the first plus lane 0 of the second, lane 1 plus lane 1, and so on, all four additions happening as one instruction rather than four separate f32.adds. Nothing about the stack machine changes to support this — v128 is just a fifth value type alongside i32, i64, f32, f64, and f32x4.add is just another instruction that declares what it pops and pushes, exactly like every instruction the flagship lesson introduced. The mechanism is unchanged; the payload per instruction got wider.

That widening matters most for workloads that already do the same scalar operation across a large uniform buffer — audio and video processing, image filters, physics simulation, machine learning kernels. Code that would otherwise loop over an array applying one operation per element can instead process four (or more, chaining vector ops) elements per instruction, which is a large, real speedup exactly where it's needed: numeric kernels that are memory-bandwidth-bound and repetitive by nature. A compiler targeting WASM SIMD either auto-vectorizes suitable loops or the source code uses explicit SIMD intrinsics that map directly onto these instructions, the same "language already thinks this way, WASM just gives it a matching target" story the linear memory lesson told about C pointers and flat memory.

Threads: shared memory plus atomics

SIMD speeds up one lane of execution. Threads unlock genuinely running more than one lane at once — real multicore parallelism, not just wider instructions.

The mechanism has two parts, and both matter. First, a WASM memory can be declared shared, backed on the JavaScript side by a SharedArrayBuffer instead of a plain ArrayBuffer. Where an ordinary ArrayBuffer transferred between two Web Workers is copied or handed off exclusively to one side, a SharedArrayBuffer is genuinely the same block of bytes visible to every worker that holds a reference to it — write a value in one worker, read the updated value from another, no copying and no message-passing round trip involved. Instantiate the same WASM module in multiple Web Workers, each pointed at that one shared memory, and you have several threads of WASM execution all reading and writing the identical linear memory the second lesson in this module described — just now with more than one thread doing it concurrently.

Sharing memory across threads immediately raises the obvious problem: two threads writing the same bytes at the same time without coordination is a race, and ordinary i32.load/i32.store give you no way to guard against that. So the second part of the threads proposal is a family of atomic instructionsi32.atomic.rmw.add, memory.atomic.wait, memory.atomic.notify, and others — that do read-modify-write or wait/wake operations as a single indivisible step the hardware guarantees won't be interleaved with another thread's atomic operation on the same address. These are the same primitives a native threaded program built on mutexes or lock-free algorithms already relies on; WASM threads expose them directly rather than inventing a new synchronization model.

Threads connect directly back to a concern from earlier in this track: the frame budget lesson established that the main thread has roughly sixteen milliseconds per frame to do everything — layout, paint, your JavaScript, and any WASM call you make from it — and a single long-running synchronous WASM call blocks all of it, frame drops included, exactly like a long JS function would. Running WASM in a Web Worker off the main thread was already one answer to that; WASM threads make that answer scale, because now several workers can share one linear memory and split a large computation between them with atomics as the coordination primitive, instead of each worker being an isolated island that can only report back through postMessage.

The rest of the roadmap, briefly

A handful of other proposals round out where WASM is headed, each solving a specific gap this module already surfaced.

Garbage collection (WASM GC) lets a module describe managed heap types — structs and arrays the engine itself tracks and collects — directly in the WASM type system, rather than the module bringing its own allocator and collector compiled into linear memory. This is the direct fix for the problem the toolchain lesson raised about garbage-collected source languages: a Java, Kotlin, or Dart program compiling to WASM without GC support has to ship its own garbage collector as part of the binary, inflating size and duplicating work the host engine could otherwise do natively. With WASM GC, the engine's own collector manages the module's objects the same way it already manages JS objects, and languages with a managed runtime can compile to WASM without dragging their whole runtime along as dead weight.

Exception handling adds first-class instructions for throwing and catching, so languages with exceptions in their source model (C++, and eventually others) don't have to compile that control flow down to manual error-code checks and branches — a mismatch between the source language's model and what the target could express directly.

Tail calls guarantee that a function call in tail position reuses the current stack frame instead of growing the call stack, which matters enormously for languages and compilation strategies (functional languages, some compiled interpreters) that rely on deep tail recursion never overflowing the stack.

Memory64 extends linear memory's addressing from 32-bit offsets to 64-bit ones, lifting the roughly 4GB ceiling core WASM memory is bound by today — relevant for workloads that outgrow that ceiling, like large in-memory datasets or big scientific computing kernels.

Stack switching provides low-level primitives for suspending and resuming a computation's stack, which is the building block async/await-style control flow and coroutines need — letting a WASM module yield control back to its host mid-computation and resume later, instead of the module's own call stack having to run to completion once entered.

None of these change the core model this whole track was built on. Every one of them is an addition on top of the same stack machine and the same linear memory, in the same spirit as SIMD: new instructions and new types serving specific needs, not a different execution model underneath.

The synthesis: the whole module, in one arc

Step back and the eight lessons in this module trace a single, continuous argument. WASM starts as a stack machine — a bytecode designed to be compact, validate in a single linear pass, and translate cheaply onto real hardware. It gets linear memory to keep that stack machine from being merely a calculator — a flat byte array that turns out to be exactly the mental model C and Rust already use for pointers, so compiling to WASM barely requires those languages to bend. A toolchain compiles real source languages down to that stack-and-memory model, hitting real friction for languages that assume a managed runtime the stack machine doesn't provide. The JS/WASM boundary shows the consequence of a value system with only four numeric types: everything richer crosses as bytes in memory and an offset, by convention, not by any built-in marshaling. Why WASM is fast ties the static types and the stack-machine design to genuinely predictable, near-native performance, with none of the type-check branches or deopt risk a dynamically-typed language carries. The streaming compilation lesson showed that same stack-machine validation running in a single linear pass is exactly what lets a browser compile a module while it's still downloading, tiering from a fast baseline compiler to an optimizing one, inside a sandbox that starts at zero authority and grants only what it's explicitly given. Beyond the browser showed that sandbox generalizing cleanly to WASI's capability-based syscalls and the component model's typed cross-language composition, because none of it ever depended on a browser being the host. And this lesson closes the arc: SIMD widens what one instruction can do, threads let multiple instances of the same machine share one memory safely, and GC, exceptions, tail calls, memory64, and stack switching are the last gaps between "a minimal, portable stack machine" and "a genuinely complete target for whatever a real program needs."

The throughline underneath all eight lessons is the same: WASM's design keeps paying off precisely because it started from a small, honest, mechanically simple core, and every later capability — speed, safety, portability, parallelism — turned out to be something you could build on that core rather than something that required replacing it.

Where this goes from here

This module told one coherent story about how a .wasm file actually works, from the instruction encoding up through the browser's compiler pipeline and out past the browser entirely. The rest of the "under the hood" curriculum keeps doing exactly this for other systems you use daily without seeing — the same instinct that took WebAssembly apart here works just as well pointed at a rendering pipeline, a database's storage engine, or a network protocol: find the small mechanical core, and let everything else fall out as a consequence of it.

Go deeper

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. Explain why f32x4.add doesn't require any change to the stack machine's execution model, only an addition to it.
  2. What two mechanisms, together, make WASM threads work, and what specific problem does each one solve on its own?
  3. Why was SharedArrayBuffer restricted after Spectre, and what two response headers does a page need to get it back?
  4. Connect WASM threads back to the frame-budget lesson: what does running a heavy WASM computation across several Worker threads let the main thread avoid?
  5. What problem does the WASM GC proposal solve for a language like Kotlin or Dart that core WASM, without it, does not solve?
  6. Pick any two post-MVP features from this lesson (SIMD, threads, GC, exceptions, tail calls, memory64, stack switching) and explain how each is 'an addition on top of the same stack machine' rather than a different execution model.
  7. Trace the module's whole arc in one or two sentences per lesson: what did each of the eight lessons add, and what earlier lesson's mechanism did it build directly on top of?