How Browsers Load and Run WebAssembly
A browser loads a WebAssembly module by fetching its .wasm binary and passing it to WebAssembly.instantiateStreaming, which validates and compiles the bytecode while it is still downloading rather than waiting for the full file. Validation is a strict type-check of every instruction before anything runs, so a Wasm module either passes as a whole or is rejected outright — there is no partial execution of malformed bytecode the way a browser might try to recover from broken HTML.
Cricket analogy: It's like a pitch inspection before a Test match: the entire surface is checked and certified fit for play before a single ball is bowled, rather than starting and stopping mid-over.
The JS <-> Wasm Bridge
Once instantiated, a Wasm module's exported functions can be called directly from JavaScript like any other function, but only primitive numeric types — i32, i64, f32, f64 — cross the boundary natively. Strings, objects, and arrays have no native Wasm representation, so tools like wasm-bindgen generate glue code that writes bytes into linear memory and passes an offset and length instead, hiding the manual pointer arithmetic a developer would otherwise have to write by hand.
Cricket analogy: Umpires communicate a simple raw signal — a raised finger for out — rather than a full paragraph explanation, similar to how only primitive numbers cross the JS/Wasm boundary directly.
async function loadMath() {
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/math.wasm')
);
const { add, memory } = instance.exports;
console.log(add(2, 3)); // 5 - primitive numbers cross directly
// Reading a UTF-8 string the module wrote into its own memory
const ptr = instance.exports.greet();
const len = instance.exports.greet_len();
const bytes = new Uint8Array(memory.buffer, ptr, len);
console.log(new TextDecoder('utf-8').decode(bytes));
}
loadMath();Sharing Memory and the DOM Limitation
A module's linear memory is a WebAssembly.Memory object backed by a resizable ArrayBuffer, which is what makes zero-copy TypedArray sharing with JS possible. Wasm code, however, has no direct way to touch the DOM, make a fetch call, or set a timer — every one of those effects still has to be routed through an imported JavaScript function, which is why frameworks like Blazor (C#) and Yew (Rust) that compile whole UIs to Wasm still ship a JS interop shim underneath.
Cricket analogy: A non-striker can see the whole ground but can't umpire their own run-out decision — that authority has to be routed through the actual official, just as DOM access has to be routed through JS.
The component model and interface-types proposals aim to reduce hand-written marshaling glue by standardizing how modules describe higher-level types like strings and records across language boundaries, while the reference-types proposal already lets Wasm hold opaque references to JS objects (like a DOM node) without copying them into linear memory.
Performance Characteristics in Real Pages
V8 compiles Wasm in two tiers — Liftoff produces a fast baseline compile almost instantly so the module is runnable quickly, and TurboFan later recompiles hot functions with heavier optimization, similar in spirit to JS's JIT tiers but without the risk of deoptimizing back to an interpreter mid-execution. None of this changes the fact that a Wasm binary still has to be downloaded before it can run, so bundle size directly affects time-to-interactive just as it does for a large JS bundle.
Cricket analogy: A team fields a solid opening pair to get through the new-ball overs safely, then brings in the specialist stroke-players once the ball softens — a two-tier approach to the innings.
Shipping a large Wasm binary without streaming compilation or code-splitting can block time-to-interactive just as badly as an oversized JS bundle would — and if your actual bottleneck is DOM layout thrashing or excessive reflows rather than raw computation, moving code to Wasm won't fix it at all.
- WebAssembly.instantiateStreaming validates and compiles a .wasm module while it downloads, and validation is all-or-nothing before any code runs.
- Only primitive numbers (i32/i64/f32/f64) cross the JS/Wasm boundary natively; strings and objects need manual or generated marshaling.
- wasm-bindgen and similar tools generate glue code that writes complex data into linear memory and passes offset/length pairs.
- Wasm has no direct DOM, fetch, or timer access — every side effect routes through an imported JS function.
- V8 compiles Wasm in tiers (Liftoff baseline, then TurboFan optimizing), giving fast startup without JS's deoptimization risk.
- Bundle size still matters: a large Wasm binary delays time-to-interactive just like an oversized JS bundle would.
- Wasm doesn't automatically speed up a page whose real bottleneck is DOM layout or reflow rather than computation.
Practice what you learned
1. What does WebAssembly.instantiateStreaming allow a browser to do?
2. Which data types can be passed directly across the JS/Wasm function call boundary without marshaling?
3. Why can't a WebAssembly module manipulate the DOM directly?
4. What is a realistic reason a page using WebAssembly might still load slowly?
Was this page helpful?
You May Also Like
Wasm for Performance-Critical Code
How to identify, compile, and integrate performance-critical code paths as WebAssembly modules for near-native execution speed.
Wasm Outside the Browser
Running WebAssembly as a portable, sandboxed server and CLI runtime using WASI and standalone engines like Wasmtime and Wasmer.
Wasm for Plugins and Sandboxing
Using WebAssembly's memory-isolated, capability-based execution model to run untrusted third-party plugin code safely inside a host application.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics
Web DevelopmentNext.js Study Notes
JavaScript · 30 topics