WebAssembly (WASM)
โฑ๏ธ ~4-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.๐๏ธ WASM = a native app that lives inside the browser's sandbox. It runs at near-native speed because it skips JS parsing/optimization and goes straight to machine code. But it speaks in numbers, not DOM.
The contractor analogy
JavaScript (the general contractor) knows the whole building โ the DOM, the style system, the event loop. They can do everything, but they have to figure out the blueprints on the fly (JIT compilation) and sometimes guess at optimizations.
WebAssembly (the specialist subcontractor) arrives with pre-stamped, verified blueprints (.wasm binary). They go straight to work โ no interpretation needed. But they can only work in their own room (linear memory) and must hand results to the contractor to touch anything client-facing (DOM).
The handoff cost:every time the contractor asks the subcontractor for something (JSโWASM boundary call), there's a small overhead. Batch your requests โ don't call them for every nail.
ASCII: compilation pipeline
| 1 | C / C++ โโโบ Emscripten โโโบโ |
| 2 | โโโโบ .wasm binary โโโบ V8 compiler โโโบ machine code |
| 3 | Rust โโโบ wasm-pack โโโบโ โ |
| 4 | โ binary sections: |
| 5 | โ โโ type (signatures) |
| 6 | โ โโ import (JS imports) |
| 7 | โ โโ func (bodies) |
| 8 | โ โโ memory (linear mem) |
| 9 | โ โโ export (public API) |
| 10 | โผ |
| 11 | WebAssembly.instantiateStreaming(fetch('...')) |
| 12 | โ |
| 13 | instance.exports.myFunction(42) |
| 14 | โ |
| 15 | โ result (number only) โโโโโโโโโโโโโโโโโโ |
| 16 | โ |
| 17 | strings/objects go via linear memory โโโโโโโโโโ |
Why WASM is faster for number crunching
JavaScript JIT
- 1.Parse source โ AST
- 2.Compile AST โ bytecode
- 3.Interpret bytecode
- 4.Profile hot paths
- 5.JIT compile hot code
- 6.Deoptimize on type change
WebAssembly
- 1.Binary already validated
- 2.V8 AOT compiles .wasm
- 3.Executes machine code
- 4.No type speculation
- 5.SIMD vectorization
- 6.No deoptimizations
WASM type system โ only 4 numeric types
Strings, arrays, and objects do not exist at the WASM boundary. They must be encoded into linear memory and passed as a (pointer, length) pair of integers. wasm-bindgen (Rust) and Emscripten (C) automate this encoding.
Interactive Sandbox
โ Move something, see it react instantly.Pattern
Source code in C/Rust compiles to .wasm binary format. V8 compiles WASM to machine code faster than JS.
| 1 | // add.c |
| 2 | int add(int a, int b) { |
| 3 | return a + b; |
| 4 | } |
| 5 | // Compile: emcc add.c -o add.wasm |
| 6 | // Binary sections: |
| 7 | // (type) โ function signatures |
| 8 | // (import) โ imported JS functions |
| 9 | // (function) โ function bodies |
| 10 | // (export) โ exported symbols |
Performance benchmark
Challenge
Explore all 5 WASM patterns. For the compilation pattern, compare the C and Rust source code โ then see how both compile to the same JS instantiation pattern.
Why Should I Care?
โ The exact interview question + the bug it kills.Exact interview questions
Q: When is WASM faster than JavaScript?
WASM wins when: (1) there are tight numeric loops with no JS boundary crossings, (2) the algorithm needs predictable performance without JIT warmup, (3) SIMD vectorization helps (image/signal processing), (4) you're porting an existing C/C++/Rust library that's already highly optimized.
WASM loses when: (1) you cross the JS boundary frequently (overhead adds up), (2) the work involves the DOM or Web APIs, (3) V8's JIT has already optimized the hot path (simple typed loops can be comparable).
| 1 | // โ WASM excels: tight numeric loop, no boundary |
| 2 | // C: void process(float* data, int len) { ... } |
| 3 | instance.exports.process(ptr, length); // one call, WASM does all work |
| 4 | ย |
| 5 | // โ WASM loses: calling WASM in a JS loop |
| 6 | for (let i = 0; i < 1000; i++) { |
| 7 | result += instance.exports.add(i, i); // 1000 boundary crossings! |
| 8 | } |
Q: What can WASM NOT do that JS can?
WASM cannot: access the DOM directly, call Web APIs (fetch, setTimeout, localStorage) without importing them from JS, throw JS exceptions natively, create objects/strings without going through linear memory, or use garbage collection (though the GC proposal is landing in 2024+).
| 1 | // WASM must import DOM access from JS |
| 2 | const importObject = { |
| 3 | env: { |
| 4 | // JS provides DOM access to WASM |
| 5 | setElementText: (ptr, len) => { |
| 6 | const text = readString(memory, ptr, len); |
| 7 | document.getElementById('output').textContent = text; |
| 8 | }, |
| 9 | fetchData: async (urlPtr, urlLen) => { /* ... */ } |
| 10 | } |
| 11 | }; |
Q: How does WASM interface with the DOM?
WASM cannot touch the DOM directly. The pattern is: (1) WASM imports JS functions that perform DOM operations, (2) WASM calls those functions with numeric arguments, (3) JS reads string/object data from WASM's linear memory using the pointer+length convention, (4) JS updates the DOM.
Frameworks like wasm-bindgen (Rust) and Emscripten generate the JS glue code automatically, hiding this complexity behind ergonomic bindings.
| 1 | // wasm-bindgen hides the complexity: |
| 2 | // lib.rs |
| 3 | #[wasm_bindgen] |
| 4 | pub fn greet(name: &str) -> String { |
| 5 | format!("Hello, {}!", name) |
| 6 | } |
| 7 | ย |
| 8 | // JS (generated automatically by wasm-pack) |
| 9 | const { greet } = await import('./pkg/my_wasm.js'); |
| 10 | document.body.textContent = greet("world"); // works! |
Production WASM patterns
Pattern: instantiateStreaming vs instantiate
Always use WebAssembly.instantiateStreaming over instantiate. Streaming starts compiling the WASM module as bytes arrive over the network โ no need to wait for the full download before compilation starts.
| 1 | // โ Waits for full download before compiling |
| 2 | const bytes = await fetch('./module.wasm').then(r => r.arrayBuffer()); |
| 3 | const { instance } = await WebAssembly.instantiate(bytes); |
| 4 | ย |
| 5 | // โ Compiles while downloading (streaming) |
| 6 | const { instance } = await WebAssembly.instantiateStreaming( |
| 7 | fetch('./module.wasm'), importObject |
| 8 | ); |
| 9 | // Requires Content-Type: application/wasm header |
Pattern: compileStreaming + instantiate (for reuse)
If you need multiple instances of the same module (e.g., one per Web Worker), compile once to a WebAssembly.Module, then instantiate multiple times. The compiled module can be transferred to workers.
| 1 | // Compile once, instantiate many |
| 2 | const module = await WebAssembly.compileStreaming( |
| 3 | fetch('./module.wasm') |
| 4 | ); |
| 5 | ย |
| 6 | // Instantiate in multiple workers |
| 7 | workers.forEach(worker => { |
| 8 | worker.postMessage({ module }, [module]); // transferable! |
| 9 | }); |
| 10 | ย |
| 11 | // In worker: |
| 12 | self.onmessage = async ({ data }) => { |
| 13 | const instance = await WebAssembly.instantiate(data.module); |
| 14 | // ... use instance |
| 15 | }; |
The Deep Dive
โ Spec refs, engine internals, the minutiae.WASM binary format
A .wasm file is a binary encoding of the WebAssembly module. It starts with the magic bytes 0x00 0x61 0x73 0x6D("\0asm") followed by version 0x01 0x00 0x00 0x00. Then a sequence of sections, each with a type byte, a byte-length, and its content.
| 1 | ; WAT (text format) โ human-readable equivalent of .wasm |
| 2 | (module |
| 3 | (type $t0 (func (param i32 i32) (result i32))) |
| 4 | (func $add (type $t0) (param $a i32) (param $b i32) (result i32) |
| 5 | local.get $a |
| 6 | local.get $b |
| 7 | i32.add) |
| 8 | (export "add" (func $add)) |
| 9 | ) |
| 10 | ย |
| 11 | ; Compile WAT โ WASM: wat2wasm add.wat -o add.wasm |
| 12 | ; Decompile WASM โ WAT: wasm2wat add.wasm |
WASI โ WebAssembly System Interface
WASI lets WASM modules run outside the browser โ on servers, edge functions, CLIs โ with a portable system interface for file I/O, sockets, clocks, and random. The goal: compile once, run anywhere (browser, Node, Deno, server, embedded).
| 1 | # Run a WASM binary with WASI in Node.js |
| 2 | import { WASI } from 'wasi'; |
| 3 | import { readFile } from 'fs/promises'; |
| 4 | ย |
| 5 | const wasi = new WASI({ args: process.argv, env: process.env }); |
| 6 | const wasm = await WebAssembly.compile(await readFile('./app.wasm')); |
| 7 | const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject()); |
| 8 | wasi.start(instance); |
Emscripten vs wasm-pack (Rust)
Emscripten (C/C++)
+ Port existing C/C++ codebases
+ Large ecosystem (OpenCV, SQLite, ffmpeg)
+ Generates JS glue automatically
+ Supports Pthreads (WebWorkers)
โ Larger output (emscripten runtime)
โ C++ complexity bleeds through
โ Slower build iteration
wasm-pack (Rust)
+ Ergonomic wasm-bindgen bindings
+ TypeScript types auto-generated
+ Small output (no runtime overhead)
+ Excellent async/await support
โ Rust learning curve
โ Smaller existing library ecosystem
โ Compile times longer than C
Threading in WASM (SharedArrayBuffer)
WASM threads use SharedArrayBuffer + Atomics, same as JS. Requires the same COOP/COEP headers as the JS SharedArrayBuffer API. Emscripten supports Pthreads compiled to WASM threads; Rust uses rayon for data-parallel work.
| 1 | // Emscripten with Pthreads |
| 2 | // Build: emcc -O3 -pthread app.c -o app.js |
| 3 | // Generates: app.js + app.wasm + app.worker.js |
| 4 | ย |
| 5 | // The worker.js handles thread creation โ transparent to you |
| 6 | // In your app: |
| 7 | const { instance } = await WebAssembly.instantiateStreaming( |
| 8 | fetch('./app.wasm'), { env: { memory: sharedMemory } } |
| 9 | ); |
| 10 | // WASM threads map to Web Workers automatically |
WASM GC proposal โ objects without linear memory
The WASM GC proposal (shipped in Chrome 119, Firefox 120) allows WASM modules to use the JS GC for managed types โ structs, arrays, reference types. This enables compiling garbage-collected languages (Kotlin, Dart, OCaml, Swift) to WASM without bundling their own GC runtime. The resulting binaries are much smaller.
| 1 | ; WAT with GC types (new proposal) |
| 2 | (module |
| 3 | (type $point (struct |
| 4 | (field $x f64) |
| 5 | (field $y f64) |
| 6 | )) |
| 7 | (func $make-point (param f64 f64) (result (ref $point)) |
| 8 | local.get 0 |
| 9 | local.get 1 |
| 10 | struct.new $point |
| 11 | ) |
| 12 | ) |
| 13 | ; Kotlin/Wasm and Dart compile to this format |
Interview Questions
โ Real questions from real interviews โ with answers.WASM skips JS parse/JIT overhead and ships pre-validated binary that V8 compiles ahead-of-time to machine code with SIMD support and no type speculation.
i32, i64, f32, f64. Strings are encoded into linear memory and passed as a (pointer, length) pair of i32 integers.
Each JS-WASM call costs roughly 0.1โ1ฮผs. It matters when calling WASM in a tight loop โ batch work inside WASM instead.
Linear memory is a flat ArrayBuffer shared between JS and WASM. After grow(), the old buffer reference is detached โ always re-read memory.buffer.
instantiateStreaming compiles the WASM module as bytes arrive over the network (streaming); instantiate waits for the full download first.
WASM loses when work involves the DOM, frequent JS boundary crossings, or when V8's JIT has already fully optimized the hot path.
Memory Game
โ Quick quiz โ lock the concept in long-term memory.Why should you use WebAssembly.instantiateStreaming instead of WebAssembly.instantiate?