Introduction to WebAssembly

JavaScript has been the only language browsers execute natively for three decades. WebAssembly changes that. Since shipping in all major browsers in 2017, Wasm has moved from "interesting experiment" to a production tool used by Figma, Google Earth, AutoCAD, and FFmpeg.js. Here's how it actually works.
What WebAssembly Is (and Isn't)
WebAssembly is a binary instruction format for a stack-based virtual machine. You don't write Wasm by hand - you compile to it from C, C++, Rust, Go, or a dozen other languages. The browser downloads the .wasm binary, validates it (fast, since it's a well-defined format), and runs it.
A few things Wasm is not:
- It's not a replacement for JavaScript. Wasm can't touch the DOM directly. It needs JS to call
document.getElementByIdand friends. - It's not automatically faster than JS for everything. Wasm wins on predictable, compute-heavy work. JS is usually faster for DOM manipulation and event handling.
- It's not unsafe. Wasm runs in a sandbox - it only has access to memory you explicitly give it.
The Memory Model: Linear Memory
This is the key concept. A Wasm module has access to a linear memory - a flat byte array it fully controls. From the Wasm side it looks like i32 offsets into an ArrayBuffer. From the JS side it's the same buffer:
// Loading a .wasm module
const response = await fetch('/my-module.wasm');
const buffer = await response.arrayBuffer();
const module = await WebAssembly.instantiate(buffer, {});
const { memory, add, allocate } = module.instance.exports;
// Read from Wasm memory
const view = new Uint8Array(memory.buffer);
console.log(view[0]); // byte at offset 0
// Write into Wasm memory
view[100] = 42;
The Wasm module cannot access anything outside this buffer. No window, no document, no filesystem. This isolation is fundamental to both security and portability.
Compiling Rust to Wasm: The Practical Path
Rust has the best Wasm toolchain of any language right now. The wasm-bindgen crate generates the JS/Wasm glue automatically:
# Install target
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
// src/lib.rs
use wasm_bindgen::prelude::*;
// Exported to JavaScript
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u64 {
match n {
0 => 0,
1 => 1,
_ => {
let mut a = 0u64;
let mut b = 1u64;
for _ in 2..=n {
let c = a + b;
a = b;
b = c;
}
b
}
}
}
// Pass and return strings
#[wasm_bindgen]
pub fn process_json(input: &str) -> String {
// serde_json works in Wasm
let v: serde_json::Value = serde_json::from_str(input)
.unwrap_or(serde_json::Value::Null);
format!("Processed: {} fields", v.as_object().map(|o| o.len()).unwrap_or(0))
}
Build and use:
wasm-pack build --target web
This produces a pkg/ directory with your .wasm file, a JS wrapper, and TypeScript types. Import in your app:
import init, { fibonacci, process_json } from './pkg/my_module.js';
await init(); // loads and instantiates the .wasm
console.log(fibonacci(40)); // 102334155, runs in ~0.1ms
console.log(process_json('{"a":1,"b":2}')); // "Processed: 2 fields"
Compiling C/C++ with Emscripten
For existing C/C++ codebases, Emscripten is the standard tool:
# Install
git clone https://github.com/emscripten-core/emsdk.git
./emsdk/emsdk install latest && ./emsdk/emsdk activate latest
source ./emsdk/emsdk_env.sh
// image_process.c
#include <stdint.h>
#include <stdlib.h>
// Grayscale a pixel buffer in-place
void grayscale(uint8_t* data, int width, int height) {
int num_pixels = width * height;
for (int i = 0; i < num_pixels; i++) {
int idx = i * 4;
uint8_t r = data[idx], g = data[idx+1], b = data[idx+2];
uint8_t gray = (uint8_t)(0.299f * r + 0.587f * g + 0.114f * b);
data[idx] = data[idx+1] = data[idx+2] = gray;
}
}
emcc image_process.c -o image_process.js \
-s EXPORTED_FUNCTIONS='["_grayscale", "_malloc", "_free"]' \
-s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' \
-O2
Use from JavaScript:
const Module = await import('./image_process.js');
await Module.default();
const grayscale = Module.cwrap('grayscale', null, ['number', 'number', 'number']);
// Get pixel data from canvas
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Copy into Wasm memory
const ptr = Module._malloc(imageData.data.length);
Module.HEAPU8.set(imageData.data, ptr);
// Run C function
grayscale(ptr, canvas.width, canvas.height);
// Copy back
imageData.data.set(Module.HEAPU8.subarray(ptr, ptr + imageData.data.length));
ctx.putImageData(imageData, 0, 0);
Module._free(ptr);
This pattern - malloc, copy in, compute, copy out, free - is the fundamental Wasm data exchange model for languages without GC integration.
Performance: When Wasm Actually Wins
| Task | JS | Wasm (Rust) | Speedup |
|---|---|---|---|
| Fibonacci(40) | 0.8ms | 0.1ms | 8× |
| SHA-256 (1MB) | 14ms | 2.4ms | 5.8× |
| JPEG decode (2MP) | 120ms | 22ms | 5.5× |
| String concatenation (100k ops) | 2ms | 6ms | 0.3× |
| DOM updates (1000 nodes) | 8ms | 40ms (via JS bridge) | 0.2× |
The pattern is clear: Wasm wins on CPU-bound number crunching. It loses when you're crossing the JS/Wasm boundary frequently (each crossing has overhead) or doing DOM work.
Debugging Wasm
Chrome DevTools supports debugging Wasm directly. For Rust, add the wasm-pack build --dev flag to keep debug symbols, then in DevTools → Sources, you'll see your .rs source files. You can set breakpoints and inspect variables.
For production builds, source maps are supported but .wasm binaries are stripped. Use wasm-opt -O3 from the Binaryen toolkit to reduce binary size (often 30-50%):
wasm-pack build --release
wasm-opt -O3 -o pkg/my_module_opt.wasm pkg/my_module_bg.wasm
What Comes Next: WASI
WebAssembly started in the browser, but the WebAssembly System Interface (WASI) extends it to servers. WASI gives Wasm modules a standardized way to access files, networking, and environment variables, while keeping the same sandbox model. A Wasm module compiled for WASI can run in Cloudflare Workers, Fermyon Spin, or a local wasmtime process without recompilation.
Server-side Wasm is a different topic from browser Wasm, but the fundamentals - linear memory, sandbox isolation, binary portability - are identical.
You Might Also Like
- WebAssembly Deep Dive: Rust, Wasmtime, and the Extism Plugin System
- WebAssembly (Wasm) Beyond the Browser: A New Era of Compute
- WebAssembly in 2026: WASI, Component Model, and Running Wasm in Production
- Memory Safety in Zig
Test Your Knowledge
A team is developing a high-performance web application that needs to process large image files client-side. They are considering using WebAssembly for the core image manipulation logic. What is the most critical factor to consider when deciding between WebAssembly and JavaScript for this specific task?
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

WebAssembly with C++
Compile high-performance C++ code to WebAssembly using Emscripten: learn memory management, JavaScript interop bindings, and near-native browser speed.
Read more
WebAssembly in 2026: Beyond the Browser | WASI, Edge, and Plugin Systems
WebAssembly beyond the browser in 2026: compile Rust to WASI, embed Wasmtime in Python, build sandboxed plugin systems with Extism, and deploy to Cloudflare Workers and Fermyon Spin.
Read more
WebAssembly Beyond the Browser: Building High-Performance Microservices
Explore how to use WebAssembly on the server side with Wasmtime, WasmEdge, and Spin to build near-native speed, language-agnostic, and capability-sandboxed microservices - with real benchmarks vs Docker containers.
Read more