| name | wasm-performance |
| description | WebAssembly performance: wasm-opt binary optimization, size reduction (panic=abort, LTO, strip), profiling WASM in Chrome DevTools, memory management (linear memory, avoiding GC pressure), SIMD, and multi-threading with SharedArrayBuffer. |
WebAssembly Performance
Techniques for reducing WASM binary size and maximizing runtime performance.
When to Activate
- Binary is too large for acceptable load time
- Profiling reveals WASM execution bottlenecks
- Memory usage is growing unexpectedly
- Implementing compute-intensive features in WASM
- Enabling SIMD or multi-threading for heavy workloads
- Reducing JS↔Wasm boundary crossings by batching calls or sharing linear memory directly
- Using
twiggy or cargo-bloat to identify which crate or function is dominating binary size
Binary Size Optimization
Cargo Profile (highest impact)
[profile.release]
opt-level = "s"
lto = true
panic = "abort"
codegen-units = 1
strip = true
wasm-opt (Binaryen)
cargo install wasm-opt
wasm-opt -Os -o output.wasm input.wasm
wasm-opt -O3 -o output.wasm input.wasm
wasm-opt -Oz --enable-mutable-globals -o output.wasm input.wasm
wasm-opt -O3 --enable-simd -o output.wasm input.wasm
wasm-pack build --release
twiggy — Dependency Size Analyzer
cargo install twiggy
twiggy top target/wasm32-unknown-unknown/release/my_lib.wasm
twiggy paths target/wasm32-unknown-unknown/release/my_lib.wasm
cargo-bloat — Rust Bloat Detector
cargo install cargo-bloat
cargo bloat --release --target wasm32-unknown-unknown -n 20
cargo bloat --release --target wasm32-unknown-unknown --crates
Reducing Dependency Footprint
[dependencies]
serde = { version = "1", default-features = false, features = ["derive"] }
chrono = { version = "0.4", default-features = false, features = ["serde"] }
Runtime Performance
Avoid JS↔Wasm Boundary Crossings
Every call across the JS/Wasm boundary has overhead. Batch work:
#[wasm_bindgen]
pub fn process_item(item: u32) -> u32 {
item * 2 + 1
}
#[wasm_bindgen]
pub fn process_batch(items: &[u32]) -> Vec<u32> {
items.iter().map(|&x| x * 2 + 1).collect()
}
Share Memory Instead of Copying
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn alloc(size: usize) -> *mut u8 {
let mut buf: Vec<u8> = Vec::with_capacity(size);
let ptr = buf.as_mut_ptr();
std::mem::forget(buf);
ptr
}
#[wasm_bindgen]
pub fn dealloc(ptr: *mut u8, size: usize) {
unsafe { drop(Vec::from_raw_parts(ptr, 0, size)) }
}
#[wasm_bindgen]
pub fn process_buffer(ptr: *const u8, len: usize) -> u32 {
let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
slice.iter().map(|&b| b as u32).()
}
const memory = wasm_instance.memory as WebAssembly.Memory;
const ptr = alloc(data.length);
new Uint8Array(memory.buffer, ptr, data.length).set(data);
const result = process_buffer(ptr, data.length);
dealloc(ptr, data.length);
SIMD (WebAssembly SIMD)
use std::arch::wasm32::*;
#[cfg(target_arch = "wasm32")]
pub fn dot_product_simd(a: &[f32], b: &[f32]) -> f32 {
assert_eq!(a.len(), b.len());
let mut sum = f32x4_splat(0.0);
let chunks = a.len() / 4;
for i in 0..chunks {
let a_vec = unsafe { v128_load(a.as_ptr().add(i * 4) as *const v128) };
let b_vec = unsafe { v128_load(b.as_ptr().add(i * 4) as *const v128) };
sum = f32x4_add(sum, f32x4_mul(a_vec, b_vec));
}
let arr: [; ] = { std::mem::(sum) };
arr.().()
}
RUSTFLAGS="-C target-feature=+simd128" wasm-pack build --release
wasm-opt -O3 --enable-simd output.wasm -o output_simd.wasm
Multi-Threading with SharedArrayBuffer
[dependencies]
wasm-bindgen-rayon = "1.0"
use wasm_bindgen_rayon::init_thread_pool;
#[wasm_bindgen]
pub async fn setup_threads() {
init_thread_pool(4).await;
}
#[wasm_bindgen]
pub fn parallel_sum(data: &[f64]) -> f64 {
use rayon::prelude::*;
data.par_iter().sum()
}
<meta http-equiv="Cross-Origin-Opener-Policy" content="same-origin">
<meta http-equiv="Cross-Origin-Embedder-Policy" content="require-corp">
Memory Management
Linear Memory Growth
#[wasm_bindgen]
pub fn memory_usage() -> usize {
core::arch::wasm32::memory_size(0) * 64 * 1024
}
#[wasm_bindgen]
pub fn ensure_capacity(bytes: usize) {
let current = core::arch::wasm32::memory_size(0) * 65536;
if bytes > current {
let pages_needed = (bytes - current + 65535) / 65536;
core::arch::wasm32::memory_grow(0, pages_needed);
}
}
Avoid Memory Leaks
#[wasm_bindgen]
pub fn leaky(size: usize) -> *mut u8 {
let mut v: Vec<u8> = vec![0; size];
let ptr = v.as_mut_ptr();
std::mem::forget(v);
ptr
}
#[wasm_bindgen]
pub fn create_buffer(size: usize) -> *mut u8 {
Box::into_raw(vec![0u8; size].into_boxed_slice()) as *mut u8
}
#[wasm_bindgen]
pub unsafe fn free_buffer(ptr: *mut u8, size: usize) {
let _ = Box::from_raw(std::slice::from_raw_parts_mut(ptr, size));
}
Profiling
Chrome DevTools
console.time in Rust
use web_sys::console;
#[wasm_bindgen]
pub fn benchmark_process(data: &[u8]) -> u32 {
console::time_with_label("process");
let result = expensive_computation(data);
console::time_end_with_label("process");
result
}
Counting Operations
use std::sync::atomic::{AtomicU64, Ordering};
static OPS_COUNT: AtomicU64 = AtomicU64::new(0);
pub fn hot_function(x: u64) -> u64 {
OPS_COUNT.fetch_add(1, Ordering::Relaxed);
x * x + x + 1
}
#[wasm_bindgen]
pub fn get_ops_count() -> u64 {
OPS_COUNT.load(Ordering::Relaxed)
}
Size Benchmarks
| Technique | Savings |
|---|
opt-level = "s" | 20–40% |
lto = true | 10–30% |
panic = "abort" | ~50KB |
wasm-opt -Os | 10–20% |
strip = true | 20–60% |
| Remove unused features | varies |
Typical final size for a medium-complexity library:
- Before optimization: 500KB–2MB
- After all techniques: 50KB–200KB
Loading Strategy by Size
| Binary Size | Strategy |
|---|
| < 100KB | Inline as base64 or bundle |
| 100KB–1MB | Streaming instantiation (WebAssembly.instantiateStreaming) |
| > 1MB | Lazy load on demand + loading indicator |
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/assets/my_lib.wasm'),
importObject
);
async function loadWasm(url: string) {
try {
return await WebAssembly.instantiateStreaming(fetch(url));
} catch {
const bytes = await (await fetch(url)).arrayBuffer();
return WebAssembly.instantiate(bytes);
}
}
Reference