← Back to Lessons Lesson 26 of 28
Intermediate getting-started
Wasm Performance Profiling
Why Profile Wasm?
Wasm is fast, but not automatically optimal. Common issues:
- Binary too large — slow initial load
- Excessive boundary crossings — copying strings/data JS↔Wasm
- Unnecessary allocations — creating Vec/String per frame
- Unoptimized algorithms — O(n²) when O(n) is possible
Measuring Binary Size
# Check raw size
ls -lh pkg/my_app_bg.wasm
# See what's taking space
cargo install twiggy
twiggy top pkg/my_app_bg.wasm
# Example output:
# Shallow Bytes │ Shallow % │ Item
# ───────────────┼───────────┼──────────────
# 12,458 │ 15.32% │ data[0]
# 8,234 │ 10.13% │ wasm_bindgen::convert
# 5,120 │ 6.30% │ core::fmtReducing Binary Size
Cargo.toml optimizations
[profile.release]
opt-level = "z" # Optimize for size (smallest)
lto = true # Link-time optimization
codegen-units = 1 # Better optimization
strip = true # Remove debug symbols
panic = "abort" # Smaller panic handlingwasm-opt (10-30% further reduction)
# Install binaryen
npm install -g binaryen
# Optimize
wasm-opt -Oz -o small.wasm pkg/my_app_bg.wasm
# Compare
ls -lh pkg/my_app_bg.wasm small.wasmAvoid heavy dependencies
| Crate | Size impact | Alternative |
|---|---|---|
serde_json |
+50-100KB | serde-wasm-bindgen (0KB, uses JsValue) |
regex |
+100-200KB | Manual string parsing |
chrono |
+50KB | js_sys::Date (free, uses JS Date) |
rand |
+30KB | js_sys::Math::random() (free) |
Runtime Profiling
console.time (simple)
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn time(label: &str);
#[wasm_bindgen(js_namespace = console, js_name = "timeEnd")]
fn time_end(label: &str);
}
#[wasm_bindgen]
pub fn expensive_operation() {
time("expensive_operation");
// ... your code ...
time_end("expensive_operation");
}performance.now (precise)
const start = performance.now();
wasm_function();
const elapsed = performance.now() - start;
console.log(`Took ${elapsed.toFixed(2)}ms`);Browser DevTools Profiler
- Open DevTools → Performance tab
- Click Record
- Interact with your app
- Stop recording
- Look for
wasm-function[N]in the flame chart - Sort by "Self Time" to find bottlenecks
Benchmarking Best Practices
// BAD: single measurement (noisy)
const t = performance.now();
result = wasm_fn();
console.log(performance.now() - t);
// GOOD: multiple iterations, warm up
function bench(fn, iterations = 1000) {
// Warm up (JIT, caches)
for (let i = 0; i < 10; i++) fn();
const start = performance.now();
for (let i = 0; i < iterations; i++) fn();
const elapsed = performance.now() - start;
return elapsed / iterations;
}Common Optimization Patterns
1. Batch boundary crossings
// BAD: N boundary crossings
for item in items {
let result = process(item); // JS calls Wasm N times
display(result); // Wasm returns to JS N times
}
// GOOD: 1 boundary crossing
let results = process_all(items); // One call, one return2. Reuse allocations
// BAD: allocates every call
pub fn get_data(&self) -> Vec<f64> {
self.items.iter().map(|i| i.value).collect()
}
// GOOD: reuse buffer
pub fn get_data(&mut self, output: &mut [f64]) {
for (i, item) in self.items.iter().enumerate() {
output[i] = item.value;
}
}Size Targets
| Category | Size | Quality |
|---|---|---|
| < 50KB | Excellent | Loads in < 100ms |
| 50-150KB | Good | Acceptable for most apps |
| 150-500KB | Okay | Consider lazy loading |
| > 500KB | Large | Needs optimization |
| > 1MB | Too large | Split or reduce dependencies |
Try It
Click Run to see benchmarking in action. The code compares a loop vs formula approach and measures fibonacci — demonstrating how to identify performance differences.
Try It
Chapter Quiz
Pass all questions to complete this lesson