Why WebAssembly exists
JavaScript already runs everywhere. So why did browser vendors agree to ship a second, lower-level execution target — and why is it now showing up in CDNs, plugin systems, and AI runtimes?
On this page
- The picture version
- Why it exists
- Why it matters now
- The short answer
- How it works
- 1. The format is structured for fast, predictable compilation
- 2. Memory is a sandboxed linear buffer
- 3. The module can do nothing until the host imports give it powers
- What WASM is not
- The seam worth staring at
- Famous related terms
- Going deeper
The picture version
Five pictures for a reader who has run Figma in a browser tab without wondering how. The prose below fills in the seams the pictures skip.
1 · The problem
A C++ routine wants to run in your tab. Neither obvious route works.
2 · Design choice one
Hand the runtime a program whose types are already pinned down.
3 · Design choice two
A room with no doors, and one hatch the host controls.
4 · What that bought, off the browser
Everyone who wanted lighter-than-a-container isolation.
5 · Keep this card
The whole thing on one index card.
Why it exists
You’ve opened Figma, or Google Earth, or an emulator playing a Nintendo game, in a browser tab. No install, no plugin, no “this site wants to run Java.” Twenty years ago each of those needed a downloaded application or a plugin that regularly turned into a security incident. Something changed in between, and it wasn’t that JavaScript got fast enough.
We’ll keep one concrete thing in view for the whole post: a C++ image-processing routine that a photo editor wants to run in your tab. Follow it, and the design falls out.
Before WebAssembly, that routine got there by a faintly perverse route. Look at any heavy in-browser app from the mid-2010s — photo editor, 3D game, CAD tool, emulator — and you find people writing the performance-critical code in C or C++, then compiling it through a chain of tools into a giant blob of JavaScript that the browser would re-parse, re-optimize, and run. The blob worked, but everything about it was strained. Parse times were huge. Number types were wrong (JavaScript only had doubles, so a C int32 had to be emulated). The JIT spent ages re-deriving facts the C++ compiler already knew and had thrown away. The format was a workaround.
The workaround had a name — asm.js — and it worked well enough that browser vendors looked at it and asked the obvious question: if we are already pretending JavaScript is a compile target, why not ship a real one?
That real one is WebAssembly. It exists because the web needed a way to run code that wasn’t written in JavaScript without paying the JavaScript tax — the parse cost, the type ambiguity, the dynamic dispatch — and without re-opening the security holes that NPAPI plugins like Flash and Java applets had spent two decades demonstrating.
Why it matters now
WASM started as a browser story but the part engineers actually trip over today is what happened off the browser. The WebAssembly spec defined a portable binary format with one interesting property: the runtime is a sandbox that gives the guest code zero ambient capabilities by default. No filesystem. No network. No clock. Anything the module can do, the host has to explicitly hand to it.
That property — capability-by-default-nothing — turns out to be exactly what a lot of non-browser systems were quietly looking for:
- Edge compute. Fastly Compute is WASM-native end-to-end. Cloudflare Workers is a different shape — its primary isolation boundary is V8 isolates, with WASM as a supported guest inside that — but the underlying point is the same: both want lighter-than-a-container isolation, and both treat WASM as a first-class way to ship customer code.
- Plugin systems. Envoy and Istio expose a
proxy-wasmextension API; Spin and similar runtimes treat WASM modules as the deployment unit. The pattern is the same: replace “load this.soand pray” with “load this WASM module, and by design the worst it can do is misuse the API I gave it.” (Lots of other plugin ecosystems are experimenting with WASM; the dominant story in editors and creative tools is still native or scripting.) - Universal binaries. The same
.wasmfile is portable across CPU architectures and operating systems. Browser-vs-server portability is fuzzier — that depends on which imports the module uses (Web APIs vs. WASI) — but the within-host portability story is unusually clean. - AI tooling. ONNX Runtime Web, transformers.js, and llama.cpp’s WASM build run inference inside the browser tab. On the server side, WASI-flavored runtimes are being explored as sandboxes for model-serving and agent tool execution, though nobody publishes deployment figures, so treat the browser-side uses as the documented ones.
The question “why does WASM exist” has a 2015 answer (browsers needed a real compile target) and a 2026 answer (the industry needed a portable sandbox). Both are true; the second is what most engineers will actually run into.
The short answer
WebAssembly = portable bytecode + capability-based sandbox + near-native speed
Picture to keep: a sealed room with no doors and no windows, and a single hatch through which the host passes in exactly the tools it chooses. Our image routine gets handed a buffer of pixels and a way to hand pixels back. It cannot ask for a file, a socket, or the time, because there is nothing in the room to ask with.
WASM is a stack-machine bytecode designed to be (a) compiled to from C, C++, Rust, Go, and friends, (b) verified and JIT-compiled by the host quickly and predictably, and (c) executed inside a sandbox that gets exactly the imports the host chooses to grant it and nothing else. The browser was the first host. It is no longer the only one.
How it works
Try to get our C++ image routine into a browser tab yourself, and watch each attempt fail.
Attempt 1: just ship the compiled machine code. Fastest possible option, and a non-starter twice over. It’s x86 — useless on an ARM phone. And it’s native code with a flat view of memory, which means a bug in it (or a malicious version of it) owns the machine. This is roughly the deal NPAPI plugins offered, and the industry spent two decades paying for it.
Attempt 2: compile it to JavaScript. Portable, sandboxed, no new spec. This is asm.js, and it genuinely worked — but see the failures in the hook: megabytes of text to parse, the JIT re-deriving types the compiler already knew, and integer math emulated on top of doubles. You’re describing a machine to a language that was designed to hide machines.
The fix is a real compile target, and three design choices carry it.
1. The format is structured for fast, predictable compilation
A .wasm module is a binary file with explicit sections: types, imports, functions, memory, exports. Functions are encoded as typed stack-machine instructions — i32.add, f64.mul, local.get, call $foo. Types are static; every value at every point in the program has a known type the validator can check in one linear pass.
This is the part that JavaScript, by design, can’t do. JS’s type story is “figure it out as you go,” which forces the JIT to speculate, deoptimize, and re-specialize. WASM hands the runtime a program where the types are already pinned down — our image routine’s int32 pixel arithmetic arrives as i32.add, not as a double the JIT has to prove is really an integer. The result is that hosts can validate and compile a WASM module — JIT, AOT, or interpret, depending on the host — much more quickly and predictably than equivalent JS, which is what made it usable in browsers as a streaming compile target in the first place.
But solving speed reopens the security problem from attempt 1. Fast, statically typed code operating on raw memory is exactly what made native plugins dangerous. So:
2. Memory is a sandboxed linear buffer
The classic mental model — and still the common case in 2026 — is that a WASM module sees its memory as one contiguous, host-allocated ArrayBuffer (in the browser) or equivalent block elsewhere. (A multi-memory extension exists in modern engines, but the per-memory shape is the same.) All loads and stores are bounds-checked against that buffer. Our image routine can scribble anywhere in its own pixel buffer, including out past the end of its own arrays — that’s a bug, and it corrupts its own image. It cannot address anything outside the buffer: not other modules, not host memory, not the kernel.
This is what makes the sandbox cheap. There’s no need for a separate process or a hardware page table per guest; the bounds checks plus the lack of raw pointers to host objects do the work that an MMU usually does. (Modern engines elide most of those bounds checks at compile time using tricks like guard pages on 64-bit hosts, so the runtime cost in practice is small.)
But memory safety isn’t the whole attack surface. A module confined to its own buffer could still, if the runtime let it, open a socket or read a file. So:
3. The module can do nothing until the host imports give it powers
A freshly loaded WASM module has no syscalls, no fetch, no clock, no console.log. All of those are imports the host has to bind at instantiation time. If you don’t pass env.write_file, the module cannot write a file — there is no other way for it to try. This is the capability model, and it’s the property the non-browser hosts care about.
WASI is the attempt to standardize a set of these imports — a portable, capability-style “syscall” surface — so the same module can run in different non-browser runtimes. There are two release lines worth knowing: WASI Preview 1, the older shape that’s still widely used in production toolchains, and WASI 0.2 / Preview 2, which is built on top of the WebAssembly component model (a separate spec layer that adds typed interfaces between modules). Preview 1 and Preview 2 are not source-compatible. Which line dominates real deployments, and how fast Preview 2 is displacing Preview 1, isn’t something anyone publishes numbers on.
What WASM is not
- It is not a Java applet. Applets had a giant standard library bundled into the runtime — UI toolkits, networking, file access — and security bugs in any of that became browser-pwning bugs. WASM ships no standard library. The capabilities come from the host, one function at a time.
- It is not a JavaScript replacement. WASM has no native string type, no garbage collector for high-level languages by default (the GC proposal is shipping but adoption is uneven), and no DOM access. Calling into the DOM means calling back out through JS. WASM is good at the parts JS is bad at, and bad at the parts JS is good at.
- It is not magically faster than native. Bounds checks, indirect-call checks, and the limits of single-pass JITs leave a real gap to native AOT-compiled code on the same CPU. The interesting comparison is “WASM vs. JS” and “WASM vs. a container,” not “WASM vs. unsandboxed native.”
The seam worth staring at
The single most underrated thing about WASM is what’s not in the spec. There is no I/O. No threading model in v1 (threads came later, behind a feature flag, and required browser-level coordination on shared memory). No string type. No exceptions originally. No 64-bit integers in the JS interop layer for years. Every one of those omissions was deliberate, and most of them have since been addressed as separately negotiated extensions — exceptions, SIMD, BigInt interop, GC. Some are still in flight: a first-class string type is a live proposal rather than a settled feature, and threads remain partly proposal-shaped even though browsers ship them.
That’s the engineering bet WASM made: ship a tiny, conservative core that can be implemented and verified by every browser quickly, and let the ecosystem argue over the extensions. Compare that to the way Java, Flash, and ActiveX shipped — huge runtime, huge attack surface, “trust us.” The minimalism is the security story. It’s also why writing real software in WASM still leans heavily on toolchain-provided runtimes (Emscripten, wasi-libc, wasm-bindgen) to paper over the gaps.
You started with WebAssembly = portable bytecode + capability-based sandbox + near-native speed. What did the image routine add? — + the sandbox is a consequence of what was left out, not a feature that was added. There’s no permission system to configure and no policy engine to get wrong; the module can’t reach the filesystem because no function that reaches the filesystem exists unless the host passes one in. That’s also the prediction to carry away: whenever someone proposes adding a capability to the core spec rather than to the imports, that’s the moment the security story starts eroding.
Famous related terms
- asm.js —
asm.js = strict JS subset designed to JIT like C— the proof of concept that talked browser vendors into shipping a real compile target. - WASI —
WASI = capability-based syscall API for non-browser WASM— what turns “browser sandbox” into “portable server sandbox.” Two incompatible release lines are still in circulation. - Component model —
component model = WASM modules + typed interfaces + a canonical ABI— the long-running effort to let modules in different source languages compose against shared interfaces instead of bespoke glue. - Containers —
container ≈ process + namespaces + cgroups— the heavier-weight isolation primitive WASM is increasingly an alternative to for short-lived, untrusted code. - eBPF —
eBPF = sandboxed bytecode running in the kernel— a cousin in spirit; small verifiable bytecode, capability-bounded, used for a totally different host (the Linux kernel). - JIT —
JIT = compile bytecode to machine code at runtime— the technique that makes WASM fast in browsers, and the reason WASM’s static types matter.
Going deeper
- Bringing the Web up to Speed with WebAssembly (Haas et al., PLDI 2017) — the primary source from the team that designed the format; read it for the rationale behind each omission, which the spec states but doesn’t justify.
- Lin Clark’s A cartoon intro to WebAssembly (2017) and Standardizing WASI: A system interface to run WebAssembly outside the web (2019), Mozilla Hacks — the clearest non-academic answer to “what does this actually look like in practice.”
- The WebAssembly Core Specification at the W3C — the rabbit hole for anyone who wants to see how small the core really is; the binary format section is surprisingly short.