← 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::fmt

Reducing 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 handling

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

Avoid 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

  1. Open DevTools → Performance tab
  2. Click Record
  3. Interact with your app
  4. Stop recording
  5. Look for wasm-function[N] in the flame chart
  6. 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 return

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