๐Ÿณ IntuitiveFE
Login
โ† All concepts

WebAssembly (WASM)

โฑ๏ธ ~4-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

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

js
1C / C++ โ”€โ”€โ–บ Emscripten โ”€โ”€โ–บโ”
2 โ”œโ”€โ”€โ–บ .wasm binary โ”€โ”€โ–บ V8 compiler โ”€โ”€โ–บ machine code
3Rust โ”€โ”€โ–บ 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

i32
i64
f32
f64

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.

js
1// add.c
2int 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

JavaScript12ms (parse + JIT optimize)
WebAssembly1ms (pre-compiled binary, direct machine code)
โš ๏ธ Gotcha: WebAssembly.instantiate() is async โ€” you must await it before calling exports. Forgetting this is the #1 WASM bug.
๐Ÿ’ก Insight: WASM binaries skip parsing and most JIT overhead because the format is already validated and close to machine code. V8 compiles WASM ahead of time using its own compiler tier.
Viewed:โš™๏ธ๐Ÿ”Œ๐Ÿ’พ๐ŸŽ๏ธ๐ŸŒ
๐ŸŽฏ

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.

Try it
๐ŸŽฏ

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

js
1// โœ“ WASM excels: tight numeric loop, no boundary
2// C: void process(float* data, int len) { ... }
3instance.exports.process(ptr, length); // one call, WASM does all work
4ย 
5// โœ— WASM loses: calling WASM in a JS loop
6for (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+).

js
1// WASM must import DOM access from JS
2const 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.

js
1// wasm-bindgen hides the complexity:
2// lib.rs
3#[wasm_bindgen]
4pub fn greet(name: &str) -> String {
5 format!("Hello, {}!", name)
6}
7ย 
8// JS (generated automatically by wasm-pack)
9const { greet } = await import('./pkg/my_wasm.js');
10document.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.

js
1// โœ— Waits for full download before compiling
2const bytes = await fetch('./module.wasm').then(r => r.arrayBuffer());
3const { instance } = await WebAssembly.instantiate(bytes);
4ย 
5// โœ“ Compiles while downloading (streaming)
6const { 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.

js
1// Compile once, instantiate many
2const module = await WebAssembly.compileStreaming(
3 fetch('./module.wasm')
4);
5ย 
6// Instantiate in multiple workers
7workers.forEach(worker => {
8 worker.postMessage({ module }, [module]); // transferable!
9});
10ย 
11// In worker:
12self.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.

js
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).

js
1# Run a WASM binary with WASI in Node.js
2import { WASI } from 'wasi';
3import { readFile } from 'fs/promises';
4ย 
5const wasi = new WASI({ args: process.argv, env: process.env });
6const wasm = await WebAssembly.compile(await readFile('./app.wasm'));
7const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject());
8wasi.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.

js
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:
7const { 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.

js
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.
1/4

Why should you use WebAssembly.instantiateStreaming instead of WebAssembly.instantiate?