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) = -6

That'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 @+0

One 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.

Frequently Asked Questions

Will WebAssembly eventually replace JavaScript? No — they're designed to run together in the same engine, with JavaScript still owning the DOM, events, and application orchestration while WebAssembly handles specific CPU-heavy workloads passed to it.
Can a WebAssembly module access the DOM directly? No. A WASM module has no direct access to the DOM, network, or storage; any interaction with those has to go back through an imported JavaScript function.
Do I need to know Rust or C++ to use WebAssembly? No — plenty of languages compile to WASM, and as this post shows, you can even write the raw bytes of a valid module by hand without any source language at all.
Is WebAssembly always faster than JavaScript? No. For short, small tasks, a JIT-warmed JavaScript engine is often just as fast or faster once you account for WASM's download, compile, and boundary-crossing overhead; the real advantage shows up in sustained, CPU-heavy computation.
Can WebAssembly run outside the browser? Yes — standalone runtimes like Wasmtime and Wasmer, and built-in engines like Node's WebAssembly global used in this post, all run WASM modules without a browser involved at all.