What you'll learn
- Quick answer
- What WebAssembly Actually Is
- Why You'd Compile to WebAssembly Instead of Writing JavaScript
- Inside a .wasm File: We Hand-Built One and Ran It
- How JavaScript and WebAssembly Actually Talk to Each Other
- How This Actually Gets Built in Practice: Rust, C/C++, and Emscripten
- What WebAssembly Still Can't Do, and When It's Worth Reaching For
- FAQ
Quick Answer
WebAssembly (WASM) is a binary instruction format that runs in a sandboxed virtual machine inside the browser (and outside it), letting you execute code compiled from languages like C, C++, Rust, and Go at near-native speed alongside JavaScript. It doesn't replace JavaScript — it runs next to it, handling the CPU-heavy parts of an app while JS still owns the DOM and orchestration. We hand-assembled a real 41-byte WASM module and ran it live in Node to show exactly what's inside one.
What WebAssembly Actually Is
WebAssembly is a low-level binary instruction format designed from day one as a portable compilation target — a format compilers can emit and any conforming engine can run, regardless of which source language produced it. It's built around a stack machine: a simple execution model where instructions push and pop values from an implicit operand stack instead of naming registers, which is exactly what makes a WASM module fast to validate and simple enough to compile even with a basic, non-optimizing baseline compiler before a more aggressive JIT kicks in.
It is not a replacement for JavaScript, and that's not a hedge — it's a structural fact about how it runs. A WASM module executes inside the same JavaScript engine (V8 in Chrome and Node, SpiderMonkey in Firefox, JavaScriptCore in Safari), in a sandboxed linear memory space, with zero direct access to the DOM, the network, storage, or any Web API. Every interaction with the outside world has to be explicitly wired up through JavaScript, via a set of imports and exports negotiated when the module is instantiated. WASM and JS are two instruction sets sharing one host environment, not two competing runtimes.
It's also an open, W3C-ratified standard, not a single vendor's project, and every major browser has shipped a conforming engine since 2017. More recently, standalone runtimes like Wasmtime and Wasmer — and, as this post demonstrates, Node's own built-in WebAssembly global — run WASM modules completely outside any browser, which is why WASM has also become a serious option for sandboxed plugin systems and portable server-side execution, not just a browser performance trick.
Why You'd Compile to WebAssembly Instead of Writing JavaScript
The honest case for WASM is narrow and specific: sustained, CPU-bound work where a dynamically-typed, garbage-collected, JIT-warmed language pays a real and measurable tax. Video and audio codecs, image and signal processing, physics simulation, cryptographic operations, and general-purpose compression are the classic examples — all of them tight numeric loops over typed data, exactly the kind of code that a language like Rust or C++ compiles into predictable, branch-light machine instructions with no GC pause waiting to happen mid-loop.
The second, equally common reason has nothing to do with raw speed: reusing code that already exists and already works. Companies with a large C or C++ codebase, or a mature Rust crate ecosystem, can recompile that existing, tested logic to run in the browser instead of rewriting it from scratch in JavaScript — which is a very different, much lower-risk proposition than a rewrite.
Figma is a real, documented example of exactly this: its rendering engine is written in C++ and compiled to WebAssembly using Emscripten, and the 2017 migration of that engine to WASM — led by co-founder Evan Wallace — cut the application's load time by roughly 3x, independent of how large the document being opened was. That's not a synthetic microbenchmark; it's a shipped product decision that held up in production, and it's the shape of problem WASM is actually good for: an existing, performance-critical native engine, ported once, running in a browser tab.
Inside a .wasm File: We Hand-Built One and Ran It
Rather than describe the binary format in the abstract, we built a real, minimal WASM module by hand — 41 bytes, no compiler involved — that exports a single function, add(a, b), and actually ran it in Node 22's built-in WebAssembly global. Every module starts with a 4-byte magic number (the bytes for \0asm) and a 4-byte version number, followed by a sequence of named sections — Type, Function, Export, and Code are the four this module needs, each one a section-id byte, a length, and its own encoded content.
const bytes = new Uint8Array([
0x00, 0x61, 0x73, 0x6d, // magic: "\0asm"
0x01, 0x00, 0x00, 0x00, // version 1
// Type section (id=1): one function type (i32,i32) -> i32
0x01, 0x07,
0x01, // 1 type
0x60, // functype tag
0x02, 0x7f, 0x7f, // 2 params, both i32
0x01, 0x7f, // 1 result, i32
// Function section (id=3): 1 function, using type index 0
0x03, 0x02,
0x01, 0x00,
// Export section (id=7): export "add" as function index 0
0x07, 0x07,
0x01,
0x03, 0x61, 0x64, 0x64, // name length 3, "add"
0x00, 0x00, // kind=func, index=0
// Code section (id=10): body for function 0
0x0a, 0x09,
0x01, // 1 function body
0x07, // body size = 7 bytes
0x00, // 0 local declarations
0x20, 0x00, // local.get 0
0x20, 0x01, // local.get 1
0x6a, // i32.add
0x0b // end
]);The Type section declares the function's signature — two 32-bit integers in, one 32-bit integer out. The Function section links the module's one function body to that signature. The Export section is what makes add callable from JavaScript at all, by name. And the Code section is the actual program: local.get 0 and local.get 1 push the two parameters onto the operand stack, i32.add pops both and pushes their sum, and 0x0b (end) closes the function. No registers, no variable names at the binary level — just a stack and a short sequence of opcodes.
Here's the real, unedited output from actually instantiating and calling it:
Module byte length: 41
Exported functions: [ 'add' ]
add(3, 4) = 7
add(19, 23) = 42
add(-10, 4) = -6That's not a simulation of what a .wasm file contains — it's an actual conforming module, built directly from the binary format spec, validated and executed by a real production WebAssembly engine.
How JavaScript and WebAssembly Actually Talk to Each Other
WebAssembly's value types are deliberately minimal: i32, i64, f32, f64, plus newer reference types for more advanced use cases. There is no string type, no object, no array crossing the JS-WASM boundary as a first-class value — only numbers. Anything more complex has to be serialized into WASM's linear memory, a single contiguous ArrayBuffer that both sides can read and write, with JS passing an integer offset (a pointer, in effect) into that shared buffer instead of passing the data directly. Tooling like wasm-bindgen for Rust automates this marshalling so you rarely write the pointer arithmetic by hand, but the mechanism underneath is exactly this.
The engine also validates a module's binary structure deterministically before running a single instruction, and this is easy to see for real. WebAssembly.validate(bytes) on our module returns true; corrupting a single byte of the magic number and re-running it returns false — and attempting to instantiate that corrupted module throws a real, specific error rather than silently misbehaving:
WebAssembly.validate(bytes) = true
WebAssembly.validate(corrupted) = false
instantiate(corrupted) threw: CompileError - WebAssembly.instantiate(): expected magic word 00 61 73 6d, found 99 61 73 6d @+0One more real result worth knowing before you ever call a WASM function from JS with anything other than a clean integer: add(2.9, 3) actually returns 5, not 5.9. JavaScript numbers crossing into an i32 parameter get coerced through ToInt32, which truncates toward zero — 2.9 becomes 2, and 2 + 3 is 5. It's a real, easy-to-hit gotcha the moment your JS side does floating-point math and then hands the result to a WASM function expecting an integer.
How This Actually Gets Built in Practice: Rust, C/C++, and Emscripten
To be upfront: this environment doesn't have rustc, cargo, wasm-pack, or emcc installed, so rather than fabricate compiler output we can't actually produce, the module above was assembled by hand directly from the binary format specification and then genuinely executed. It is exactly the same file format any real toolchain emits — the difference is only in who wrote the bytes, not what the engine does with them.
In practice, almost nobody hand-writes WASM bytes past a learning exercise like this one. The Rust toolchain, via wasm-pack and wasm-bindgen, compiles a Rust crate straight to a .wasm binary and generates a matching JavaScript glue file that automates exactly the memory-marshalling described above — passing strings and structs across the boundary without you managing raw pointers yourself. For C and C++, Emscripten is the standard toolchain: it compiles existing native code (including genuinely large, real-world codebases like ffmpeg) to WASM, and generates a JS runtime that emulates enough of a POSIX-like environment — a virtual filesystem, standard I/O — that code never written with the browser in mind can often be ported with only modest changes. Go and .NET's Blazor each ship their own WASM compilation targets too, though with very different runtime sizes depending on how much of their standard library and garbage collector has to travel along with the compiled code.
What WebAssembly Still Can't Do, and When It's Worth Reaching For
A WASM module cannot touch the DOM, full stop. Every DOM read or write has to go back out through an imported JavaScript function, which means a UI-heavy application gains little from WASM and pays real overhead on every one of those boundary crossings — this is not the right tool for "make my React app faster."
There's also a real fixed cost before a WASM module does anything useful: the binary has to be downloaded, validated, and compiled. For a small script, a already-JIT-warmed engine running plain JavaScript is frequently just as fast, or faster, once you account for that startup cost and the overhead of crossing the JS-WASM boundary at all — WebAssembly's advantage shows up over sustained, heavy computation, not short one-off calls.
The pattern that actually pays off, mirrored by Figma's own case: a well-defined, CPU-heavy engine with a narrow, well-designed interface to the JavaScript around it — not a wholesale replacement for your application's everyday logic, DOM manipulation, or API calls, all of which JavaScript still does perfectly well.
