WierdX — Programming Reference All tutorials →
Developer reference · Practical tutorials · CS fundamentals
Web Platform

WebAssembly Explained: Running Native Code in the Browser

WebAssembly isn't a replacement for JavaScript. It's a second execution target inside the same sandbox, meant for the code where JavaScript's dynamic typing and garbage collector get in the way of raw throughput.

Published July 6, 2026

WebAssembly (wasm) is a binary instruction format designed to be a compilation target, not something people write by hand in production. You write in a language like Rust, C, C++, or Go, compile to a .wasm module, and the browser's wasm engine runs that module at speeds close to native code, because the format is already low-level and statically typed by the time it reaches the browser.

Why not just use JavaScript?

JavaScript engines like V8 are remarkably fast for a dynamically typed language, using JIT compilation and inline caches to specialize hot code paths at runtime. But that specialization has to happen every time, and it can de-optimize if a variable's shape changes unexpectedly. Wasm skips that guesswork: types are fixed at compile time, memory layout is explicit, and there's no garbage collector to pause execution (unless the source language brings its own, as with a wasm-targeted Go build). For CPU-bound work — image and video codecs, physics simulations, cryptography, ported C++ libraries — that consistency translates into real, measurable speedups.

What it looks like from the JavaScript side

A wasm module doesn't replace your app; it gets loaded and called from JavaScript like any other dependency. The standard loading path is WebAssembly.instantiateStreaming, which fetches and compiles the module in one step:

WebAssembly.instantiateStreaming(fetch('math.wasm'))
  .then(result => {
    const { add, multiply } = result.instance.exports;
    console.log(add(2, 3));       // 5
    console.log(multiply(4, 5));  // 20
  });

The functions exported from the module show up as plain JavaScript functions you call normally. Passing simple numbers across that boundary is cheap; passing strings or complex objects is not, because wasm's linear memory is just a flat byte array — there's no native concept of a JavaScript string or object inside it. Tooling like wasm-bindgen for Rust generates the glue code that serializes strings into that byte array and back, but every crossing still has a real cost, which is why wasm pays off most when you call it once with a large payload rather than thousands of times with small ones.

Getting there from Rust

The common path for Rust is wasm-pack, which compiles a crate to wasm and generates a JavaScript-friendly package:

// lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => {
            let (mut a, mut b) = (0u64, 1u64);
            for _ in 2..=n {
                let next = a + b;
                a = b;
                b = next;
            }
            b
        }
    }
}
wasm-pack build --target web

That produces a .wasm binary plus a JS shim that handles instantiation and type conversion, importable directly in a browser module.

The sandbox model

Wasm code runs inside the exact same security sandbox as JavaScript — it has no direct access to the filesystem, network, or DOM. Every capability it needs (fetching a resource, manipulating the page) has to be explicitly imported from JavaScript and called back into. This is a deliberate design choice documented in the MDN WebAssembly reference: wasm is fast, not privileged. A malicious wasm module is no more dangerous than a malicious JavaScript file, because it's bound by the same origin and capability restrictions.

Where it doesn't help

DOM manipulation, event handling, and most everyday UI logic get no benefit from wasm and are often slower, because every DOM call still has to cross back into JavaScript. Startup cost matters too — compiling and instantiating a wasm module isn't free, so shipping a large module for a feature that runs once on page load can cost more than it saves. Wasm earns its keep on sustained, computation-heavy work: a spreadsheet engine, a video encoder, a game's physics loop, a compression library. If your bottleneck is network requests or DOM reflows, wasm won't touch it.

Beyond the browser

Wasm's sandboxing and portability turned out to be useful outside browsers entirely. WASI (the WebAssembly System Interface) defines a standard way for a wasm module to request filesystem, clock, and networking access from whatever runtime hosts it, without being tied to a browser's JavaScript APIs. That's made wasm attractive as a plugin format — letting untrusted third-party code run inside a database, a CDN edge function, or a game engine with the same sandboxing guarantees it has in a browser tab, and without needing a full container or VM just to isolate it. Runtimes like Wasmtime and Wasmer exist specifically for this server-side and embedded use case, separate from any browser.

A realistic size check

Before reaching for wasm, it's worth actually measuring the JavaScript version first. A tight, well-optimized JavaScript loop handling a few thousand iterations is often fast enough that the added build complexity of a Rust or C toolchain, plus the WebAssembly.instantiateStreaming startup cost, isn't worth it. Wasm's advantage compounds with input size and iteration count — it tends to matter once you're processing megabytes of data or running millions of iterations per interaction, not for the kind of logic most web apps run on every keystroke.