Skip to content

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:

Terminal window
# wasm-opt (standalone or post-compile)
wasm-opt -Oz input.wasm -o output.wasm
# Rust via wasm-pack
wasm-pack build --release # wasm-opt -Oz runs automatically
# Emscripten
emcc -Oz source.c -o output.wasm
# TinyGo
tinygo build -opt=z -target wasm -o output.wasm ./main.go

Debug information (DWARF, name sections, producer metadata) can easily double the binary size. Strip it for production.

Terminal window
# Strip all custom sections with wasm-opt
wasm-opt -Oz --strip-debug --strip-producers output.wasm -o output.stripped.wasm
# Or with wasm-tools
wasm-tools strip output.wasm -o output.stripped.wasm
# Measure the effect
wc -c output.wasm output.stripped.wasm

Keep an unstripped copy on your build server so you can symbolicate crash reports.

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:

Terminal window
# Cargo.toml
[dependencies]
wee_alloc = "0.4"
# lib.rs — replace the global allocator
Terminal window
# Build (wasm-pack handles the rest)
wasm-pack build --release --target web

For 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.

application/wasm
# Serve with the correct MIME type

In JavaScript:

Terminal window
// 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.

Optimisation without measurement is guesswork. Keep these three metrics in your CI pipeline:

Terminal window
# 1. Binary size (raw and gzip)
wc -c output.wasm
gzip -c output.wasm | wc -c
# 2. Size breakdown by function
twiggy top output.wasm | head -20
# 3. Validate the optimised binary has not been broken
wasm-validate output.wasm
wasm-tools validate output.wasm

A regression in any of these three metrics after a dependency update is a signal to investigate before shipping.

Which wasm-opt flag minimises binary size most aggressively?
What is the benefit of WebAssembly.instantiateStreaming over instantiate?
What does --strip-debug remove from a binary?
Which technique eliminates the allocator entirely for Rust modules?