Home › Programming languages › Specialized
guide low

WebAssembly

Problem When is it worth compiling code to WebAssembly instead of writing JavaScript or native code? This guide helps decide for fast kernels in the browser, sandboxed plugins, or sharing one codebase across platforms.

A portable, sandboxed bytecode that browsers and server runtimes compile to near-native code; pick it for hot kernels, safe plugins and one codebase everywhere, not for DOM glue.

wasmwebassemblywasisandboxcomponent-modelsimdrustbrowseredge
Overview

WebAssembly is a typed stack-machine bytecode that engines validate and compile ahead of execution. A module sees only its own bounds-checked linear memory plus whatever functions the host imports, so untrusted code runs in-process without ambient access to files or network. You compile Rust, C/C++ or Zig to it, then call it from JavaScript or a native host.

Key facts

Use when

  • Hot numeric kernels in web apps (codecs, parsers, image work); sandboxed plugins; one core shared by browser, server and mobile.
  • Real uses by field: browser design tools (Figma cut its load time 3x with Wasm), CDN and edge serverless (Fastly Compute runs Wasm; Cloudflare Workers run Wasm modules), network proxies (Envoy extensions).

Avoid when

  • The work is DOM-bound or tiny, or is massively parallel (use WebGPU).

Deep dive

Decision table

SituationChoiceWhy
Hot kernel in a web pageRust/C++ to wasm32-unknown-unknown, one call per bufferPredictable speed; boundary cost paid once
DOM, fetch, UI glueJavaScript/TypeScriptWasm has no DOM access; every call goes through JS
Untrusted user plugins on a serverWasmtime with memory limits and epoch interruptionNo ambient authority; runaway code is stoppable
Same validation logic in browser and backendOne Rust crate, native + wasm buildsNo drift between implementations
Many short-lived requests (edge functions)AOT-compiled module, pooling allocatorInstantiation avoids compiling and copying memory
Microcontroller or tiny hostInterpreter (wasmi, WAMR)Small footprint, no JIT memory
Kotlin/Dart UI in the browserWasm GC targetsThe engine's collector replaces a bundled one
Threads in the browserShared memory plus cross-origin isolationSharedArrayBuffer needs COOP/COEP headers

Code

Hand-emitted module (no libraries): one memory, sum(ptr, len) -> i32. It shows the whole binary format: header, sections, LEB128, structured control flow. Compiled with stable Rust and run in Node: sum(100,4) over bytes 1,2,3,250 returned 256, validate returned true.

fn leb(mut v: u32, out: &mut Vec<u8>) {            // unsigned LEB128
    loop {
        let b = (v & 0x7f) as u8;
        v >>= 7;
        if v == 0 { out.push(b); return; }
        out.push(b | 0x80);
    }
}
fn section(id: u8, body: &[u8], out: &mut Vec<u8>) {
    out.push(id);
    leb(body.len() as u32, out);
    out.extend_from_slice(body);
}
fn name(s: &str, out: &mut Vec<u8>) {
    leb(s.len() as u32, out);
    out.extend_from_slice(s.as_bytes());
}
fn module() -> Vec<u8> {
    let mut m = b"\0asm\x01\0\0\0".to_vec();       // magic + version 1
    section(1, &[1, 0x60, 2, 0x7f, 0x7f, 1, 0x7f], &mut m); // type: (i32,i32)->i32
    section(3, &[1, 0], &mut m);                    // function 0 uses type 0
    section(5, &[1, 0x00, 1], &mut m);              // memory, min 1 page
    let mut e = vec![2];
    name("mem", &mut e); e.extend([2, 0]);          // export kind 2 = memory
    name("sum", &mut e); e.extend([0, 0]);          // export kind 0 = function
    section(7, &e, &mut m);
    // locals: 0 = ptr, 1 = len, 2 = acc
    let body: &[u8] = &[
        1, 1, 0x7f,                                  // one extra i32 local
        0x02, 0x40, 0x03, 0x40,                      // block, loop
        0x20, 1, 0x45, 0x0d, 1,                      // if len == 0 break
        0x20, 2, 0x20, 0, 0x2d, 0, 0, 0x6a, 0x21, 2, // acc += load8_u(ptr)
        0x20, 0, 0x41, 1, 0x6a, 0x21, 0,             // ptr += 1
        0x20, 1, 0x41, 1, 0x6b, 0x21, 1,             // len -= 1
        0x0c, 0, 0x0b, 0x0b,                         // continue; end loop; end block
        0x20, 2, 0x0b,                               // return acc
    ];
    let mut c = vec![1];
    leb(body.len() as u32, &mut c);
    c.extend_from_slice(body);
    section(10, &c, &mut m);
    m
}
fn main() {
    let mut b = Vec::new();
    leb(624485, &mut b);
    assert_eq!(b, [0xe5, 0x8e, 0x26]);
    let m = module();
    std::fs::write("sum.wasm", &m).unwrap();
}

Host side (Node and browsers share this API); also run:

const { instance } = await WebAssembly.instantiate(bytes);  // browsers: instantiateStreaming(fetch(url))
const { mem, sum } = instance.exports;
new Uint8Array(mem.buffer).set([1, 2, 3, 250], 100);        // copy in, then ONE call
sum(100, 4);                                                // 256

Guest-side pattern for real crates (the exports compile and pass asserts natively; build for Wasm with cargo build --release --target wasm32-unknown-unknown):

#[unsafe(no_mangle)]
pub extern "C" fn alloc(len: usize) -> *mut u8 {
    let mut v = Vec::<u8>::with_capacity(len);
    let p = v.as_mut_ptr();
    std::mem::forget(v);
    p
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dealloc(ptr: *mut u8, cap: usize) {
    unsafe { drop(Vec::from_raw_parts(ptr, 0, cap)) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn checksum(ptr: *const u8, len: usize) -> u32 {
    let s = unsafe { std::slice::from_raw_parts(ptr, len) };
    s.iter().fold(0u32, |a, &b| a.wrapping_mul(31).wrapping_add(b as u32))
}

Size-oriented release profile (Cargo.toml), then wasm-opt -Oz in.wasm -o out.wasm:

[profile.release]
opt-level = "s"      # or "z"; measure, "3" is sometimes faster
lto = true
codegen-units = 1
panic = "abort"
strip = true

Complexity and constants

  • Compile cost is roughly linear in module size (V8 Liftoff article); V8 reported 39.5 MB modules taking over 30 s to start on many machines (2018) before its baseline compiler.
  • Steady state: native-class for numeric kernels, but SPEC CPU 2019: 45 % (Firefox) to 55 % (Chrome) slower on average, peak 2.08x and 2.5x. Earlier work reported about 10 % on small scientific kernels.
  • An interpreter is 20x or more slower than compiled Wasm (V8's own measurement), so interpret only where JIT is impossible.
  • Every JS-to-Wasm call crosses a boundary; only numbers pass directly, everything else is copied through linear memory.
  • Memory only grows, never shrinks; grow detaches existing ArrayBuffer views.

Efficiency

  • Cost: compiled Wasm ran SPEC CPU 45-55 % slower than native on average in browsers (Jangda et al., 2019; peaks 2.08x Firefox, 2.5x Chrome); small scientific kernels came within about 10 %. An interpreter is 20x or more slower than compiled code (V8 Liftoff post). Linear memory grows in 64 KiB pages. The bound is usually boundary traffic plus download and compile time, not the kernel.
  • Levers:
    • batch across the boundary — one call per buffer or frame instead of per element (no figure) — any JS↔Wasm or host↔guest loop
    • SIMD (-C target-feature=+simd128, -msimd128) — V8's CPU hand-tracking demo went from 14-15 to 38-40 FPS (V8 SIMD post) — data-parallel kernels; feature-detect and ship two builds
    • streaming + tiered compile (instantiateStreaming, baseline tier then optimizing) — compile overlaps download (V8 Liftoff post) — large browser modules; serve application/wasm
    • compile once on the server (wasmtime compile → .cwasm, Module::deserialize), copy-on-write memory init, pooling allocator — skips compilation and most instantiation work (Wasmtime docs) — many short-lived instances
    • ship less (panic = "abort", LTO, opt-level = "s", wasm-opt, Brotli/gzip) — smaller download (no figure) — browser delivery
  • Naive → standard → state of the art:
    • naive · per-element calls across the boundary, interpreter or cold compile per request: 20x+ slower than compiled
    • standard · batched calls, optimizing compile, guard-page bounds checks: order of 1.5x native (SPEC average above)
    • state of the art · AOT-precompiled .cwasm, CoW instantiation, SIMD kernels; epoch interruption instead of fuel for runaway control; plain JS or WebGPU when the work is DOM-bound or massively parallel

Implementation notes

  • Pass strings and structs as (ptr, len) into memory you allocated through guest alloc; decode with TextDecoder. Re-create typed-array views after anything that can grow memory.
  • wasm32-unknown-unknown has no OS; use wasm32-wasip1 / wasm32-wasip2 for WASI, and wasm-bindgen when you want generated JS glue.
  • Components describe interfaces in WIT and compose across languages; core modules are the right unit for a single hot kernel.
  • Execution is mostly deterministic; NaN payloads, relaxed SIMD, threads and memory.grow failure are not. Check before using it for replay or consensus.

Pitfalls

  • Calling Wasm per element or per small string: boundary and copy cost exceeds the work.
  • Holding a Uint8Array across a memory.grow (view length becomes 0).
  • Serving .wasm without application/wasm (streaming fails) or without COOP/COEP when using threads.
  • Assuming sandbox means no resource abuse: set memory, table and time limits.
  • Expecting DOM, threads or filesystem to "just work"; each needs host support.
  • Judging speed from a micro-benchmark that ignores download and tier-up.

Variants and alternatives

In production

Browsers run it in all major engines; Figma runs its editor on it. Edge and CDN: Fastly Compute and Cloudflare Workers. Proxies: Envoy loads Wasm extensions. Server-side: Wasmtime, Wasmer, WasmEdge and WAMR embed it for plugins and edge functions. Kotlin/Wasm and Flutter's Wasm build use Wasm GC.

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
WasmtimeRustbytecodealliance/wasmtimebrew install wasmtime / cargo add wasmtime————Cranelift JIT/AOT, WASI, components, pooling
WasmerRustwasmerio/wasmerbrew install wasmer / cargo add wasmer————pluggable backends, embeds in many languages
wasm-bindgenRustwasm-bindgen/wasm-bindgencargo add wasm-bindgen————JS/Rust glue generator
Binaryen (wasm-opt)C++WebAssembly/binaryenbrew install binaryen————optimizer for size and speed
EmscriptenC++emscripten-core/emscriptenbrew install emscripten————C/C++ toolchain for Wasm and JS

Sources

  • WebAssembly Community Group, WebAssembly Specification. spec
  • A. Rossberg, Wasm 3.0 Completed, webassembly.org, 2025-09-17. news
  • A. Haas et al., Bringing the Web up to Speed with WebAssembly, PLDI 2017. paper
  • A. Jangda, B. Powers, E. Berger, A. Guha, Not So Fast: Analyzing the Performance of WebAssembly vs. Native Code, USENIX ATC 2019. paper · arXiv
  • C. Backes, Liftoff: a new baseline compiler for WebAssembly in V8, 2018. post
  • D. Gandluri, T. Lively, I. Stepanyan, Fast, parallel applications with WebAssembly SIMD, V8, 2020. post
  • Bytecode Alliance, Wasmtime Config API (memory_init_cow, guard size, fuel, epoch). docs
  • MDN, WebAssembly.instantiateStreaming() and Memory.grow(). streaming · grow
  • WASI, wasi.dev; Component Model, component-model.bytecodealliance.org
  • E. Wallace, WebAssembly cut Figma's load time by 3x, Figma blog, 2017. post
  • Fastly, Compute. docs · Cloudflare, WebAssembly (Wasm). docs · Envoy, Wasm. docs

Builds on

Used by

Systems (3):

Alternatives and related

Choosing a languageWhich programming language should you use for a given job, and when is a rewrite or a mix of languages worthwhile? This guide compares languages by speed, memory, hiring and build cost.RustIs Rust the right language when you need native speed without a garbage collector and without memory-safety bugs? This guide weighs that against its learning curve and compile times.C and C++Is C or C++ the right language for a project? This guide weighs maximum performance and the huge native ecosystem against the memory-safety and undefined-behaviour risks you must manage.ZigIs Zig a suitable C replacement for low-level systems code and cross-compiling, and what does its pre-1.0 status cost? This guide helps decide between Zig, C and Rust.TypeScript and JavaScriptIs TypeScript or JavaScript the right choice for browser UIs and for server code, and where does its single-threaded model become a problem? This guide helps decide where it fits.
webassembly runtime · plannedValidate and execute Wasm (interpreter/JIT) with linear-memory bounds and fuel limits
software fault isolation · plannedSandbox code in-process with bounds-checked or guard-page linear memory (Wasm, NaCl)
Process sandboxingSoftware that must open files from strangers, run submitted code or call a risky library worries that one bug hands the attacker everything the process can touch. The step is the moment untrusted input first reaches code.SIMD programmingHow do you speed up hot loops over arrays by processing several values per instruction, across different CPU types, while still running safely on machines that lack the newer instructions? This guide covers when and how to do it.JIT compilationA program is executed from a compact form (bytecode, an AST, a query plan, a regex) that is slow to run but starts at once. Most of it runs rarely, a small part runs constantly, and compiling everything up front costs too much startup time and memory.