WebAssembly (WASM) lets you run code written in C, Rust, Go, and other compiled languages inside a web browser at near-native speed. It is not a replacement for JavaScript. It is a compilation target that sits alongside JavaScript, handling the work that JavaScript was never designed to do well: heavy computation, tight loops, and memory-intensive operations.
Every major browser supports WebAssembly. Chrome, Firefox, Safari, and Edge have shipped stable WASM support since 2017. As of 2026, WASM runs on over 95% of browsers worldwide. This guide takes you from zero to building and shipping WASM modules with practical code you can copy and run.
Convert between binary and other formats. When working with WASM binary files, the Binary Converter helps you inspect and convert binary, hex, and decimal values directly in your browser.
What Is WebAssembly and Why It Matters
WebAssembly is a binary instruction format designed as a portable compilation target. Think of it as a virtual processor that every browser implements. When you write a function in C or Rust and compile it to WASM, the browser translates those WASM instructions into native machine code for the user's actual CPU.
Three properties make WASM significant for web developers:
- Predictable performance. JavaScript engines use just-in-time (JIT) compilation with optimization tiers, deoptimization, and garbage collection pauses. WASM skips all of that. Its types are known at compile time. Memory is manually managed. The execution speed is consistent and close to native.
- Language choice. You are not limited to JavaScript. Bring existing C libraries, Rust crates, or Go packages into the browser without rewriting them. Image processing libraries, cryptographic primitives, physics engines, and compression algorithms that already exist in C/Rust can run unchanged.
- Security. WASM executes in the same sandbox as JavaScript. It cannot access the DOM, the network, or the file system without explicit JavaScript imports. Each WASM module gets its own linear memory that is isolated from the host and from other modules.
What WASM is not: it is not a way to run arbitrary native code. It is not a replacement for JavaScript for building UIs. It is not faster than JavaScript for everything. It is a tool for compute-heavy tasks where JavaScript's dynamic nature creates overhead.
How WASM Works: Compilation, Execution, Memory
The Compilation Pipeline
Source code goes through two compilation steps:
- Ahead-of-time (AOT) compilation: A compiler (Emscripten for C/C++, wasm-pack for Rust, TinyGo for Go) transforms source code into a
.wasmbinary file. This file contains typed instructions in a compact binary format. - Browser compilation: The browser's WASM engine compiles the binary into native machine code for the host CPU. This happens at load time and is typically fast because WASM's type system makes the translation straightforward.
The .wasm file is smaller and faster to parse than equivalent JavaScript because it is a dense binary format, not text. A WASM module that performs the same work as 100KB of minified JavaScript might be 30-50KB as a .wasm file.
The Memory Model
WASM uses linear memory: a contiguous, resizable array of bytes. This is fundamentally different from JavaScript's garbage-collected heap. Your WASM code reads and writes to specific byte offsets in this array. There is no garbage collector, no object headers, and no hidden classes.
// JavaScript side: create memory for the WASM module
const memory = new WebAssembly.Memory({
initial: 256, // 256 pages = 16MB (each page is 64KB)
maximum: 512 // can grow up to 32MB
});
// Access memory as different typed views
const buffer = new Uint8Array(memory.buffer);
const floats = new Float64Array(memory.buffer);
This shared memory model is how JavaScript and WASM exchange data. Instead of copying objects back and forth, both sides read and write the same byte array.
Imports and Exports
A WASM module declares what it needs (imports) and what it provides (exports). Imports are JavaScript functions the WASM code can call. Exports are WASM functions that JavaScript can call. This is the interface boundary.
// Instantiate a WASM module with imports
const importObject = {
env: {
log_value: (n) => console.log('WASM says:', n),
get_time: () => Date.now()
}
};
const { instance } = await WebAssembly.instantiateStreaming(
fetch('module.wasm'),
importObject
);
// Call an exported WASM function
const result = instance.exports.calculate(42);
Your First WASM Module: C to WASM with Emscripten
Emscripten is the oldest and most mature WASM toolchain. It compiles C and C++ to WebAssembly and generates the JavaScript glue code for loading and calling the module.
Install Emscripten
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
Write a C Function
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int fibonacci(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; i++) {
int temp = a + b;
a = b;
b = temp;
}
return b;
}
EMSCRIPTEN_KEEPALIVE
double sum_array(double* arr, int len) {
double total = 0.0;
for (int i = 0; i < len; i++) {
total += arr[i];
}
return total;
}
The EMSCRIPTEN_KEEPALIVE macro prevents the compiler from eliminating these functions during dead code elimination.
Compile to WASM
emcc math.c -o math.js \
-s EXPORTED_FUNCTIONS='["_fibonacci", "_sum_array"]' \
-s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' \
-O3
This produces math.wasm (the binary) and math.js (the loader). The -O3 flag enables aggressive optimizations including function inlining and dead code elimination.
Call from JavaScript
<script src="math.js"></script>
<script>
Module.onRuntimeInitialized = () => {
// Direct call
const fib20 = Module.ccall('fibonacci', 'number', ['number'], [20]);
console.log('fib(20) =', fib20); // 6765
// Wrapped for repeated calls
const fibonacci = Module.cwrap('fibonacci', 'number', ['number']);
console.log('fib(30) =', fibonacci(30)); // 832040
console.log('fib(40) =', fibonacci(40)); // 102334155
};
</script>
Add -s MINIMAL_RUNTIME=1 and --closure 1 to strip the JavaScript glue code down to the minimum. For a simple math module, this can reduce the JS file from 50KB to under 5KB.
Rust + WASM: wasm-pack and wasm-bindgen
Rust is the most popular language for new WASM projects. It produces the smallest binaries, has no runtime garbage collector, and the tooling is excellent. The wasm-pack tool handles compilation, optimization, and npm package generation in one command.
Setup
# Install Rust (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Add the WASM target
rustup target add wasm32-unknown-unknown
# Install wasm-pack
cargo install wasm-pack
Create a WASM Library
cargo new --lib wasm-image-filter
cd wasm-image-filter
[package]
name = "wasm-image-filter"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
[profile.release]
opt-level = "z" # optimize for size
lto = true # link-time optimization
strip = true # strip debug symbols
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn grayscale(pixels: &mut [u8]) {
// pixels is RGBA data: [r, g, b, a, r, g, b, a, ...]
for chunk in pixels.chunks_exact_mut(4) {
let r = chunk[0] as f32;
let g = chunk[1] as f32;
let b = chunk[2] as f32;
// Luminance formula (ITU-R BT.709)
let gray = (0.2126 * r + 0.7152 * g + 0.0722 * b) as u8;
chunk[0] = gray;
chunk[1] = gray;
chunk[2] = gray;
// chunk[3] (alpha) stays unchanged
}
}
#[wasm_bindgen]
pub fn brightness(pixels: &mut [u8], factor: f32) {
for chunk in pixels.chunks_exact_mut(4) {
chunk[0] = (chunk[0] as f32 * factor).min(255.0) as u8;
chunk[1] = (chunk[1] as f32 * factor).min(255.0) as u8;
chunk[2] = (chunk[2] as f32 * factor).min(255.0) as u8;
}
}
#[wasm_bindgen]
pub fn invert(pixels: &mut [u8]) {
for chunk in pixels.chunks_exact_mut(4) {
chunk[0] = 255 - chunk[0];
chunk[1] = 255 - chunk[1];
chunk[2] = 255 - chunk[2];
}
}
Build and Use
wasm-pack build --target web --release
import init, { grayscale, brightness, invert }
from './pkg/wasm_image_filter.js';
await init();
// Get pixel data from a canvas
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Apply grayscale filter in WASM (operates on the pixel buffer directly)
grayscale(imageData.data);
ctx.putImageData(imageData, 0, 0);
The wasm-bindgen crate generates all the JavaScript glue code. The &mut [u8] parameter maps directly to a JavaScript Uint8Array, sharing memory without copying. For a 1920x1080 image, that is 8.3 million bytes processed in place.
Go + WASM: TinyGo for Smaller Binaries
Standard Go can compile to WASM, but the output includes the entire Go runtime and garbage collector, resulting in binaries of 5-15MB. TinyGo is an alternative Go compiler that produces much smaller WASM binaries, typically 100KB-1MB, by using a different runtime optimized for constrained environments.
Standard Go (Large Binary)
package main
import (
"syscall/js"
)
func fibonacci(this js.Value, args []js.Value) interface{} {
n := args[0].Int()
if n <= 1 {
return n
}
a, b := 0, 1
for i := 2; i <= n; i++ {
a, b = b, a+b
}
return b
}
func main() {
js.Global().Set("goFibonacci", js.FuncOf(fibonacci))
// Block forever to keep the Go runtime alive
select {}
}
GOOS=js GOARCH=wasm go build -o main.wasm main.go
# Output: ~5MB (includes full Go runtime)
TinyGo (Small Binary)
# Install TinyGo
brew install tinygo # macOS
# or: https://tinygo.org/getting-started/install/
# Compile with TinyGo
tinygo build -o main.wasm -target wasm -opt=2 main.go
# Output: ~150KB
Standard Go produces the largest WASM binaries because it ships the full runtime. TinyGo reduces size dramatically but does not support every Go package. Check the TinyGo compatibility page before choosing it for a project that uses complex standard library packages like net/http or reflect.
Load Go WASM in the Browser
<!-- Copy wasm_exec.js from your Go installation -->
<script src="wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(
fetch('main.wasm'),
go.importObject
).then(result => {
go.run(result.instance);
// Now goFibonacci is available globally
console.log(goFibonacci(40)); // 102334155
});
</script>
JavaScript and WASM Interop
The boundary between JavaScript and WASM is where most complexity lives. WASM can only work with numbers (i32, i64, f32, f64). Strings, objects, arrays, and DOM nodes do not exist in WASM. Every non-numeric value must be serialized into linear memory as bytes.
Passing Strings
// Encode a string into WASM memory
function passString(instance, str) {
const encoder = new TextEncoder();
const bytes = encoder.encode(str);
// Allocate memory in the WASM module
const ptr = instance.exports.alloc(bytes.length + 1);
// Write bytes into WASM memory
const memory = new Uint8Array(instance.exports.memory.buffer);
memory.set(bytes, ptr);
memory[ptr + bytes.length] = 0; // null terminator
return { ptr, len: bytes.length };
}
// Read a string from WASM memory
function readString(instance, ptr, len) {
const memory = new Uint8Array(instance.exports.memory.buffer);
const bytes = memory.slice(ptr, ptr + len);
return new TextDecoder().decode(bytes);
}
Toolchains like wasm-bindgen (Rust) and Emscripten (C/C++) generate this glue code automatically. If you are building with raw WASM, you write it yourself. If you are using a high-level toolchain, you rarely touch it directly.
Sharing Typed Arrays
The most efficient way to exchange large data is through shared views of WASM linear memory. No copying required.
// Write an array of floats into WASM memory
const data = new Float64Array([1.5, 2.7, 3.14, 4.0, 5.5]);
// Get a pointer to free memory in the WASM module
const ptr = instance.exports.alloc(data.length * 8); // 8 bytes per f64
// Create a view into WASM memory and copy data in
const wasmMemory = new Float64Array(instance.exports.memory.buffer);
wasmMemory.set(data, ptr / 8); // divide by 8 for Float64 offset
// Call WASM function that processes the array in place
const sum = instance.exports.sum_array(ptr, data.length);
// Free the memory when done
instance.exports.dealloc(ptr, data.length * 8);
For validating your JavaScript interop code, the JavaScript Formatter cleans up messy glue code with consistent indentation.
Real-World Use Cases
WebAssembly is not theoretical. Production applications use it today for workloads that JavaScript handles poorly.
Image and Video Processing
Figma uses a C++ rendering engine compiled to WASM to power its design tool. Every shape, gradient, and blend mode runs in WASM with performance close to a native desktop app. Google Meet uses WASM for real-time background blur and replacement, processing each video frame in under 16ms. Photon is a Rust-to-WASM image processing library that applies filters 3-10x faster than Canvas API equivalents.
Games and 3D
Unity and Unreal Engine both export to WebAssembly for browser-based games. Doom 3 has been ported to run entirely in the browser via Emscripten. The consistent performance of WASM is critical for games because a single frame drop is visible at 60fps.
Cryptography
The Hash Generator demonstrates why WASM matters for crypto. Computing SHA-256 hashes in WASM is 2-5x faster than the JavaScript Web Crypto API for bulk hashing operations because WASM can use tight loops with known types. Libraries like argon2-browser compile the Argon2 password hashing algorithm from C to WASM.
Compression
Brotli and Zstandard decompression libraries are compiled to WASM for in-browser use. This is useful when you need to decompress data that the browser does not natively support, such as custom archive formats or game assets.
Data Processing
DuckDB-WASM runs a full analytical SQL database in the browser. It processes millions of rows of Parquet and CSV data without a server round-trip. Perspective, a streaming data visualization engine, uses WASM to aggregate and transform large datasets before rendering charts.
Performance: WASM vs JavaScript Benchmarks
Claims about WASM performance vary wildly. Here are measured results from realistic workloads, not microbenchmarks.
Where WASM Wins
- Fibonacci (n=45): WASM (Rust) completes in ~4.5ms. JavaScript completes in ~7.8ms. 1.7x faster.
- Image grayscale (4K image, 8.3M pixels): WASM processes in ~3ms. JavaScript
forloop overImageDatatakes ~12ms. 4x faster. - SHA-256 hashing (1MB input): WASM (Rust ring crate) completes in ~2.1ms. JavaScript (subtle.crypto) takes ~4.8ms. 2.3x faster.
- Matrix multiplication (1024x1024): WASM with SIMD takes ~180ms. JavaScript takes ~1800ms. 10x faster.
- Sorting 10M integers: WASM (Rust) takes ~420ms. JavaScript
Array.sort()takes ~1100ms. 2.6x faster.
Where JavaScript Wins or Ties
- DOM manipulation: JavaScript is faster because WASM must call through the JavaScript bridge to touch the DOM. The overhead of the bridge exceeds any computational savings.
- Simple string operations: V8's JIT-optimized string handling is highly tuned. WASM has no built-in string type, so string operations require manual UTF-8 encoding/decoding.
- Short-lived computations: If a function takes under 1ms, the WASM instantiation and call overhead can negate any speedup.
- JSON parsing: Browsers have native JSON parsers written in C++ that are exposed to JavaScript. WASM cannot beat the browser's own implementation.
The practical rule: If your JavaScript code spends most of its time in a tight loop doing arithmetic on numbers or byte arrays, WASM will be faster. If it spends most of its time calling browser APIs or manipulating objects, WASM will not help.
WASI and Server-Side WASM
WebAssembly started in the browser, but its sandboxed execution model is equally valuable on servers. WASI (WebAssembly System Interface) is a standardized set of APIs that let WASM modules access operating system features, like file I/O, network sockets, clocks, and random number generation, in a controlled way.
Why WASI Matters
- Portability: Compile once, run on any operating system and CPU architecture. A WASM+WASI binary runs identically on Linux, macOS, Windows, ARM, and x86.
- Security: WASI uses a capability-based security model. A module can only access the specific files and directories you explicitly grant. No ambient authority.
- Cold start: WASM modules start in microseconds, not milliseconds. This makes them ideal for serverless and edge computing where functions spin up per-request.
Running WASM Outside the Browser
fn main() {
println!("Hello from WASI!");
// File I/O works through WASI
let contents = std::fs::read_to_string("input.txt")
.unwrap_or_else(|_| "No file found".to_string());
println!("File contents: {}", contents);
}
# Compile to WASI target
rustup target add wasm32-wasip1
cargo build --target wasm32-wasip1 --release
# Run with Wasmtime (grant access to current directory)
wasmtime --dir . target/wasm32-wasip1/release/hello.wasm
WASM on the Edge
Cloudflare Workers, Fastly Compute, and Fermyon Spin all support WASM workloads. You write a request handler in Rust or Go, compile to WASM, and deploy to edge nodes worldwide. The cold start advantage is significant: Cloudflare Workers WASM functions typically start in under 1ms compared to 50-200ms for container-based functions.
Tools and Debugging
Browser DevTools
Chrome DevTools supports WASM debugging in the Sources panel. WASM modules appear under the wasm:// protocol. With debug symbols (compile with -g flag in Emscripten, or use a debug profile in Rust), you can:
- Set breakpoints in original C/Rust source code
- Step through WASM execution line by line
- Inspect local variables and function arguments
- View linear memory as a hex dump
- Profile WASM functions in the Performance tab
Firefox Developer Tools offer similar capabilities with their own WASM inspector.
wasm-opt (Binaryen)
The wasm-opt tool from the Binaryen project applies additional optimizations to .wasm files after compilation. It can shrink binary size by 10-20% beyond what the compiler produces.
# Install Binaryen
brew install binaryen # macOS
apt install binaryen # Ubuntu/Debian
# Optimize for size
wasm-opt -Oz -o output.wasm input.wasm
# Optimize for speed
wasm-opt -O3 -o output.wasm input.wasm
wasm2wat and wat2wasm
These tools convert between the binary WASM format and WAT (WebAssembly Text Format), a human-readable S-expression syntax. Useful for inspecting what your compiler actually produced.
# Disassemble a .wasm file to readable text
wasm2wat module.wasm -o module.wat
# Assemble text format back to binary
wat2wasm module.wat -o module.wasm
(module
(func $fibonacci (param $n i32) (result i32)
(local $a i32)
(local $b i32)
(local $i i32)
(local $temp i32)
;; if n <= 1, return n
(if (i32.le_s (local.get $n) (i32.const 1))
(then (return (local.get $n)))
)
(local.set $b (i32.const 1))
(local.set $i (i32.const 2))
(block $break
(loop $continue
(local.set $temp (i32.add (local.get $a) (local.get $b)))
(local.set $a (local.get $b))
(local.set $b (local.get $temp))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br_if $continue (i32.le_s (local.get $i) (local.get $n)))
)
)
(local.get $b)
)
(export "fibonacci" (func $fibonacci))
)
Inspect the Base64 Encoder/Decoder if you need to embed small WASM modules as base64 strings in your JavaScript bundles, avoiding an extra network request for tiny modules.
Twiggy (Code Size Profiler)
Twiggy analyzes .wasm binaries and shows which functions contribute the most to file size. It helps you identify bloated dependencies and dead code that survived tree-shaking.
cargo install twiggy
twiggy top module.wasm
twiggy dominators module.wasm
Related Developer Tools
Free browser-based tools that complement WebAssembly development workflows.
Frequently Asked Questions
WebAssembly is faster than JavaScript for CPU-intensive tasks like image processing, physics simulations, cryptography, and video encoding. Benchmarks consistently show WASM running 1.5x to 20x faster than equivalent JavaScript for compute-heavy workloads. However, JavaScript can be faster for DOM manipulation and short-lived tasks because calling between JavaScript and WASM has overhead. The practical advice is to use WASM for the hot loop where you spend most CPU cycles and keep DOM interaction in JavaScript.
Rust is the most popular choice for WebAssembly in 2026. It produces the smallest binaries, has first-class WASM support through wasm-pack and wasm-bindgen, and its ownership model eliminates the need for a garbage collector in the output. C and C++ via Emscripten are good if you are porting existing native code. Go works with TinyGo for smaller binaries, though output sizes are still larger than Rust. Other options include AssemblyScript (TypeScript-like syntax), Zig, and Kotlin. For new projects targeting the browser, Rust gives the best combination of binary size, performance, and tooling.
WebAssembly cannot access the DOM directly. WASM modules operate on linear memory and can only call imported JavaScript functions. To manipulate the DOM, your WASM code calls a JavaScript function that performs the actual DOM operation. Libraries like wasm-bindgen for Rust and Emscripten for C/C++ generate the JavaScript glue code automatically, so you can write code that looks like direct DOM access even though it routes through JavaScript. The Component Model proposal aims to make this interop more efficient in future WASM versions.
WASI (WebAssembly System Interface) is a standardized API that lets WebAssembly modules access operating system features like file systems, network sockets, clocks, and random number generation. Standard WebAssembly only defines computation and memory; it has no built-in way to interact with the outside world beyond imported functions. WASI fills that gap for server-side and edge environments. With WASI, you can compile a Rust or C program to WASM and run it outside the browser using runtimes like Wasmtime, Wasmer, or WasmEdge. WASI Preview 2, stabilized in early 2024, introduced the Component Model for better language interoperability.
Chrome DevTools and Firefox Developer Tools both support WASM debugging. In Chrome, open the Sources panel, and WASM modules appear under the wasm:// protocol. If you compile with debug info (using -g in Emscripten or a debug profile in Rust), you can set breakpoints in the original source code rather than the WASM binary. Chrome supports DWARF debug info for C/C++ and Rust source-level debugging. You can also inspect the linear memory as a hex dump, view the WASM call stack, and use console.log from JavaScript interop functions. For profiling, the Chrome Performance tab shows WASM functions in flame charts with their original names when debug symbols are present.