Optimising for Production
Writing correct WebAssembly is only half the job. In production you want the binary to be as small as possible (faster downloads, warmer caches) and to start executing as quickly as possible (lower time-to-interactive). This lesson assembles the complete playbook.
Step 1 — compile with maximum size optimisation
Section titled “Step 1 — compile with maximum size optimisation”Whatever toolchain you use, enable the highest size-optimisation flag:
# wasm-opt (standalone or post-compile)wasm-opt -Oz input.wasm -o output.wasm
# Rust via wasm-packwasm-pack build --release # wasm-opt -Oz runs automatically
# Emscriptenemcc -Oz source.c -o output.wasm
# TinyGotinygo build -opt=z -target wasm -o output.wasm ./main.goStep 2 — strip debug sections
Section titled “Step 2 — strip debug sections”Debug information (DWARF, name sections, producer metadata) can easily double the binary size. Strip it for production.
# Strip all custom sections with wasm-optwasm-opt -Oz --strip-debug --strip-producers output.wasm -o output.stripped.wasm
# Or with wasm-toolswasm-tools strip output.wasm -o output.stripped.wasm
# Measure the effectwc -c output.wasm output.stripped.wasmKeep an unstripped copy on your build server so you can symbolicate crash reports.
Step 3 — reduce the allocator (Rust)
Section titled “Step 3 — reduce the allocator (Rust)”For Rust targets, the default std allocator (dlmalloc via wasm-bindgen) adds roughly 10 KB. Switching to wee_alloc or the newer lol_alloc can save several kilobytes:
# Cargo.toml[dependencies]wee_alloc = "0.4"
# lib.rs — replace the global allocator# Build (wasm-pack handles the rest)wasm-pack build --release --target webFor modules with no dynamic allocation at all, compile with #![no_std] and eliminate the allocator entirely.
Step 4 — lazy and streaming instantiation
Section titled “Step 4 — lazy and streaming instantiation”Browsers can start compiling a .wasm file while it is still downloading if you use WebAssembly.instantiateStreaming. This is always faster than fetching first and compiling second.
# Serve with the correct MIME typeIn JavaScript:
// Streaming (recommended)const { instance } = await WebAssembly.instantiateStreaming( fetch('/module.wasm'), importObject);
// Non-streaming (fallback for servers without correct MIME type)const bytes = await fetch('/module.wasm').then(r => r.arrayBuffer());const { instance } = await WebAssembly.instantiate(bytes, importObject);For large modules on repeat visits, cache the compiled WebAssembly.Module in IndexedDB to skip compilation entirely on the next load.
Step 5 — measure everything
Section titled “Step 5 — measure everything”Optimisation without measurement is guesswork. Keep these three metrics in your CI pipeline:
# 1. Binary size (raw and gzip)wc -c output.wasmgzip -c output.wasm | wc -c
# 2. Size breakdown by functiontwiggy top output.wasm | head -20
# 3. Validate the optimised binary has not been brokenwasm-validate output.wasmwasm-tools validate output.wasmA regression in any of these three metrics after a dependency update is a signal to investigate before shipping.