100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
WebAssembly

Wasm in the Browser

How browsers load, validate, compile, and execute WebAssembly modules alongside JavaScript, and how the two communicate.

Use CasesBeginner8 min readJul 10, 2026
Analogies

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.

javascript
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

Was this page helpful?

Topics covered

#WebAssembly#WebAssemblyStudyNotes#WebDevelopment#WasmInTheBrowser#Wasm#Browser#Browsers#Load#StudyNotes#SkillVeris