Skip to content
All essays
CraftMarch 20, 202518 min

WebAssembly Performance Optimization: Memory & Speed Tuning

Master WebAssembly performance optimization. Learn memory management, SIMD, threading, and benchmarking techniques.

Ü
Ümit Uz
Mobile & Full Stack Developer

Understanding WASM Performance

WebAssembly delivers near-native performance, but achieving optimal results requires understanding:

Performance Factors:
1. Memory Management
2. JavaScript Interop Overhead
3. SIMD Vectorization
4. Parallel Processing
5. Binary Size & Loading
6. Compiler Optimizations

Performance Benchmarking

Setup Benchmarks

rust
use wasm_bindgen::prelude::*;
use web_sys::Performance;

#[wasm_bindgen]
pub struct Benchmark {
    performance: Performance,
}

#[wasm_bindgen]
impl Benchmark {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Result<Benchmark, JsValue> {
        let window = web_sys::window()
            .ok_or("No window")?;
        let performance = window.performance()
            .ok_or("No performance API")?;

        Ok(Benchmark { performance })
    }

    pub fn start(&self) -> f64 {
        self.performance.now()
    }

    pub fn end(&self, start: f64) -> f64 {
        self.performance.now() - start
    }

    pub fn measure<F>(&self, f: F) -> f64
    where
        F: FnOnce(),
    {
        let start = self.start();
        f();
        self.end(start)
    }
}

JavaScript Benchmark Runner

javascript
class WasmBenchmark {
    constructor(wasmModule) {
        this.wasm = wasmModule;
    }

    async benchmark(name, fn, iterations = 1000) {
        // Warm-up
        for (let i = 0; i < 100; i++) {
            fn();
        }

        const times = [];
        for (let i = 0; i < iterations; i++) {
            const start = performance.now();
            fn();
            times.push(performance.now() - start);
        }

        const avg = times.reduce((a, b) => a + b, 0) / times.length;
        const min = Math.min(...times);
        const max = Math.max(...times);

        console.log(`${name}:`);
        console.log(`  Average: ${avg.toFixed(3)}ms`);
        console.log(`  Min: ${min.toFixed(3)}ms`);
        console.log(`  Max: ${max.toFixed(3)}ms`);

        return { avg, min, max };
    }

    compare(jsFn, wasmFn, iterations = 1000) {
        console.log('Comparing JavaScript vs WebAssembly:');

        this.benchmark('JavaScript', jsFn, iterations);
        this.benchmark('WebAssembly', wasmFn, iterations);
    }
}

// Usage
import init, * as wasm from './pkg/my_wasm.js';

await init();
const benchmark = new WasmBenchmark(wasm);

benchmark.compare(
    () => fibonacciJS(40),
    () => wasm.fibonacci_wasm(40),
    100
);

Memory Optimization

Linear Memory Management

rust
use wasm_bindgen::prelude::*;

const BUFFER_SIZE: usize = 1024 * 1024; // 1MB

#[wasm_bindgen]
pub struct MemoryBuffer {
    data: Vec<u8>,
    capacity: usize,
}

#[wasm_bindgen]
impl MemoryBuffer {
    #[wasm_bindgen(constructor)]
    pub fn new() -> MemoryBuffer {
        MemoryBuffer {
            data: Vec::with_capacity(BUFFER_SIZE),
            capacity: BUFFER_SIZE,
        }
    }

    // Allocate without copying
    pub fn allocate(&mut self, size: usize) -> usize {
        let offset = self.data.len();
        self.data.resize(offset + size, 0);
        offset
    }

    // Direct memory access
    pub fn get_ptr(&self) -> *const u8 {
        self.data.as_ptr()
    }

    pub fn get_length(&self) -> usize {
        self.data.len()
    }

    // Zero-copy operations
    pub fn write_bytes(&mut self, offset: usize, bytes: &[u8]) -> bool {
        if offset + bytes.len() > self.data.len() {
            return false;
        }

        unsafe {
            let dst = self.data.as_mut_ptr().add(offset);
            std::ptr::copy_nonoverlapping(bytes.as_ptr(), dst, bytes.len());
        }
        true
    }

    pub fn read_bytes(&self, offset: usize, length: usize) -> Vec<u8> {
        let end = (offset + length).min(self.data.len());
        self.data[offset..end].to_vec()
    }
}

Memory Pool Pattern

rust
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct MemoryPool {
    pools: Vec<Vec<u8>>,
    block_size: usize,
}

#[wasm_bindgen]
impl MemoryPool {
    #[wasm_bindgen(constructor)]
    pub fn new(block_size: usize, initial_blocks: usize) -> MemoryPool {
        let mut pools = Vec::new();
        for _ in 0..initial_blocks {
            pools.push(vec![0u8; block_size]);
        }

        MemoryPool {
            pools,
            block_size,
        }
    }

    pub fn allocate(&mut self) -> Option<usize> {
        if let Some(idx) = self.pools.iter().position(|b| b.len() == self.block_size) {
            Some(idx)
        } else {
            None
        }
    }

    pub fn deallocate(&mut self, index: usize) {
        if index < self.pools.len() {
            // Reset block
            for byte in &mut self.pools[index] {
                *byte = 0;
            }
        }
    }

    pub fn get_block(&mut self, index: usize) -> Option<&mut [u8]> {
        self.pools.get_mut(index).map(|v| v.as_mut_slice())
    }
}

Stack Allocation

rust
use wasm_bindgen::prelude::*;

// Avoid heap allocations in hot paths
#[wasm_bindgen]
pub fn process_data_avoiding_heap(data: &[u8]) -> Vec<u8> {
    // Stack-allocated buffer (when size is known)
    let mut result = Vec::with_capacity(data.len());

    for &byte in data {
        // Processing without allocations
        let transformed = byte.wrapping_add(1);
        result.push(transformed);
    }

    result
}

// Fixed-size arrays for small data
#[wasm_bindgen]
pub fn process_small_array(data: [u8; 16]) -> [u8; 16] {
    let mut result = [0u8; 16];
    for i in 0..16 {
        result[i] = data[i].wrapping_mul(2);
    }
    result
}

SIMD Optimization

Vector Operations

rust
use wasm_bindgen::prelude::*;
use std::arch::wasm32::*;

#[wasm_bindgen]
pub fn vector_add_scalar(a: &[f32], b: &[f32]) -> Vec<f32> {
    a.iter().zip(b.iter()).map(|(x, y)| x + y).collect()
}

#[wasm_bindgen]
pub fn vector_add_simd(a: &[f32], b: &[f32]) -> Vec<f32> {
    assert_eq!(a.len(), b.len(), "Arrays must have equal length");

    let mut result = vec![0.0f32; a.len()];
    let len = a.len();
    let simd_len = len / 4;

    unsafe {
        for i in 0..simd_len {
            let i = i * 4;
            let a_vec = v128_load(a.as_ptr().add(i) as *const v128);
            let b_vec = v128_load(b.as_ptr().add(i) as *const v128);
            let sum = f32x4_add(a_vec, b_vec);
            v128_store(result.as_mut_ptr().add(i) as *mut v128, sum);
        }

        // Handle remainder
        for i in (simd_len * 4)..len {
            result[i] = a[i] + b[i];
        }
    }

    result
}

// Image processing with SIMD
#[wasm_bindgen]
pub fn image_invert_simd(pixels: &mut [u8]) {
    let len = pixels.len();
    let simd_len = len / 16;

    unsafe {
        // Create mask for 255
        let mask = i8x16_splat(-1); // 0xFF when viewed as u8

        for i in 0..simd_len {
            let i = i * 16;
            let data = v128_load(pixels.as_ptr().add(i) as *const v128);

            // Subtract from 255 using bitwise NOT
            let inverted = i8x16_bitwise_not(data);

            v128_store(pixels.as_mut_ptr().add(i) as *mut v128, inverted);
        }
    }
}

Matrix Multiplication

rust
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn matrix_multiply_scalar(a: &[f32], b: &[f32], n: usize) -> Vec<f32> {
    let mut result = vec![0.0f32; n * n];

    for i in 0..n {
        for j in 0..n {
            let mut sum = 0.0f32;
            for k in 0..n {
                sum += a[i * n + k] * b[k * n + j];
            }
            result[i * n + j] = sum;
        }
    }

    result
}

#[wasm_bindgen]
pub fn matrix_multiply_simd(a: &[f32], b: &[f32], n: usize) -> Vec<f32> {
    let mut result = vec![0.0f32; n * n];

    for i in 0..n {
        for j in 0..n {
            let mut sum = [0.0f32; 4];
            let mut k = 0;

            // Process 4 elements at a time
            unsafe {
                while k + 4 <= n {
                    let a_vec = v128_load(a.as_ptr().add(i * n + k) as *const v128);
                    let b_vec = v128_load(b.as_ptr().add(k * n + j) as *const v128);

                    // Multiply and accumulate
                    let prod = f32x4_mul(a_vec, b_vec);

                    // Horizontal add (simplified)
                    let sum_vec = f32x4_add(v128_load(sum.as_ptr() as *const v128), prod);
                    v128_store(sum.as_mut_ptr() as *mut v128, sum_vec);

                    k += 4;
                }
            }

            // Handle remainder
            let mut total = sum[0] + sum[1] + sum[2] + sum[3];
            while k < n {
                total += a[i * n + k] * b[k * n + j];
                k += 1;
            }

            result[i * n + j] = total;
        }
    }

    result
}

JavaScript Interop Optimization

Minimize Boundary Crossings

rust
// ❌ Bad: Multiple boundary crossings
#[wasm_bindgen]
pub fn process_items_bad(items: &[i32]) -> Vec<i32> {
    items.iter().map(|x| x * 2).collect()
}

// ✅ Good: Single processing in WASM
#[wasm_bindgen]
pub fn process_items_good(items: &mut [i32]) {
    for item in items.iter_mut() {
        *item *= 2;
    }
}

// ✅ Even better: Batch processing
#[wasm_bindgen]
pub struct BatchProcessor {
    buffer: Vec<f32>,
}

#[wasm_bindgen]
impl BatchProcessor {
    #[wasm_bindgen(constructor)]
    pub fn new(capacity: usize) -> BatchProcessor {
        BatchProcessor {
            buffer: Vec::with_capacity(capacity),
        }
    }

    pub fn add_data(&mut self, data: &[f32]) {
        self.buffer.extend_from_slice(data);
    }

    pub fn process(&mut self) -> f64 {
        let sum: f64 = self.buffer.iter().map(|x| *x as f64).sum();
        let avg = sum / self.buffer.len() as f64;

        // Process all at once
        for val in &mut self.buffer {
            *val = (*val - avg as f32).abs();
        }

        avg
    }

    pub fn get_results(&self) -> Vec<f32> {
        self.buffer.clone()
    }
}

Use Typed Arrays

javascript
// ✅ Efficient: Direct TypedArray access
import init, { process_in_place } from './pkg/my_wasm.js';

await init();

// Create typed array
const data = new Float32Array([1, 2, 3, 4, 5]);

// Process in place (no copying)
const ptr = process_in_place(data.length);
const view = new Float32Array(wasm.memory.buffer, ptr, data.length);
view.set(data);

// Call WASM function to process
wasm.process_buffer(ptr, data.length);

// Read results
const results = new Float32Array(wasm.memory.buffer, ptr, data.length);
console.log(Array.from(results));

Multithreading with Workers

WASM Thread Setup

rust
// Cargo.toml dependencies
[dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-rayon = "1.0"
rayon = "1.8"

Parallel Processing

rust
use wasm_bindgen::prelude::*;
use rayon::prelude::*;

#[wasm_bindgen]
pub fn parallel_process(data: &[f32]) -> Vec<f32> {
    data.par_iter()
        .map(|x| x.sqrt())
        .collect()
}

#[wasm_bindgen]
pub fn parallel_filter(data: &[i32]) -> Vec<i32> {
    data.par_iter()
        .filter(|x| x % 2 == 0)
        .cloned()
        .collect()
}

#[wasm_bindgen]
pub fn parallel_sum(data: &[f32]) -> f32 {
    data.par_iter()
        .sum()
}

// Complex parallel computation
#[wasm_bindgen]
pub fn parallel_matrix_multiply(a: &[f32], b: &[f32], n: usize) -> Vec<f32> {
    let mut result = vec![0.0f32; n * n];

    result.par_chunks_mut(n)
        .enumerate()
        .for_each(|(i, row)| {
            for j in 0..n {
                let mut sum = 0.0f32;
                for k in 0..n {
                    sum += a[i * n + k] * b[k * n + j];
                }
                row[j] = sum;
            }
        });

    result
}

JavaScript Worker Integration

javascript
// main.js
const workerScript = `
    import init, { parallel_process } from './pkg/my_wasm.js';
    let wasm = null;

    self.onmessage = async (e) => {
        if (e.data.type === 'init') {
            await init();
            wasm = { parallel_process };
            self.postMessage({ type: 'ready' });
        } else if (e.data.type === 'process') {
            const { data } = e.data;
            const result = wasm.parallel_process(data);
            self.postMessage({ type: 'result', result });
        }
    };
`;

// Create workers
const numWorkers = navigator.hardwareConcurrency || 4;
const workers = [];

for (let i = 0; i < numWorkers; i++) {
    const blob = new Blob([workerScript], { type: 'application/javascript' });
    const url = URL.createObjectURL(blob);
    const worker = new Worker(url, { type: 'module' });
    workers.push(worker);
}

// Initialize all workers
await Promise.all(
    workers.map(w => new Promise(resolve => {
        w.onmessage = (e) => {
            if (e.data.type === 'ready') resolve();
        };
        w.postMessage({ type: 'init' });
    }))
);

// Distribute work
function processInParallel(data) {
    const chunkSize = Math.ceil(data.length / numWorkers);
    const promises = [];

    for (let i = 0; i < numWorkers; i++) {
        const chunk = data.slice(i * chunkSize, (i + 1) * chunkSize);
        promises.push(new Promise(resolve => {
            workers[i].onmessage = (e) => {
                if (e.data.type === 'result') resolve(e.data.result);
            };
            workers[i].postMessage({ type: 'process', data: chunk });
        }));
    }

    return Promise.all(promises).then(results => results.flat());
}

Binary Size Optimization

Compiler Flags

toml
# Cargo.toml
[profile.release]
opt-level = "z"        # Optimize for size
lto = true             # Link-time optimization
codegen-units = 1      # Better optimization
strip = true           # Remove debug symbols
panic = "abort"        # Reduce binary size

[dependencies]
wee_alloc = "0.4"      # Tiny allocator (optional)

Use wee_alloc (Small Allocator)

rust
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn hello() -> String {
    "Hello with small allocator!".to_string()
}

Code Size Optimization

rust
// ❌ Avoid: Large string formatting
#[wasm_bindgen]
pub fn format_data(data: &[i32]) -> String {
    data.iter()
        .enumerate()
        .map(|(i, x)| format!("Index {}: Value {}", i, x))
        .collect::<Vec<_>>()
        .join("\n")
}

// ✅ Prefer: Return structured data
#[wasm_bindgen]
pub fn get_data(data: &[i32]) -> Vec<i32> {
    data.to_vec()
}

Advanced Optimization Techniques

Lookup Tables

rust
use wasm_bindgen::prelude?;

// Precompute expensive operations
const SIN_TABLE: [f32; 360] = {
    let mut table = [0.0f32; 360];
    let mut i = 0;
    while i < 360 {
        table[i] = (i as f32 * 0.0174533).sin(); // degrees to radians
        i += 1;
    }
    table
};

#[wasm_bindgen]
pub fn fast_sin(degrees: i32) -> f32 {
    let degrees = degrees.rem_euclid(360) as usize;
    SIN_TABLE[degrees]
}

Cache-Friendly Algorithms

rust
use wasm_bindgen::prelude::*;

// ✅ Cache-friendly: Sequential access
#[wasm_bindgen]
pub fn sum_rows(matrix: &[f32], rows: usize, cols: usize) -> Vec<f32> {
    let mut sums = vec![0.0f32; rows];

    for i in 0..rows {
        let row_start = i * cols;
        let row_end = row_start + cols;
        sums[i] = matrix[row_start..row_end].iter().sum();
    }

    sums
}

// ❌ Cache-unfriendly: Random access
#[wasm_bindgen]
pub fn sum_cols(matrix: &[f32], rows: usize, cols: usize) -> Vec<f32> {
    let mut sums = vec![0.0f32; cols];

    for j in 0..cols {
        for i in 0..rows {
            sums[j] += matrix[i * cols + j];
        }
    }

    sums
}

Performance Monitoring

Memory Profiling

rust
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct MemoryMonitor {
    initial_memory: usize,
}

#[wasm_bindgen]
impl MemoryMonitor {
    #[wasm_bindgen(constructor)]
    pub fn new() -> MemoryMonitor {
        MemoryMonitor {
            initial_memory: wasm_bindgen::memory().size(),
        }
    }

    pub fn current_memory(&self) -> usize {
        wasm_bindgen::memory().size()
    }

    pub fn memory_used(&self) -> usize {
        self.current_memory() - self.initial_memory
    }

    pub fn memory_used_mb(&self) -> f64 {
        (self.memory_used() * 64) as f64 / 1024.0 / 1024.0
    }
}

Performance Checklist

  • [ ] Profile with browser DevTools
  • [ ] Minimize JavaScript-Wasm boundary crossings
  • [ ] Use SIMD for vector operations
  • [ ] Implement parallel processing with workers
  • [ ] Optimize memory allocation patterns
  • [ ] Reduce binary size with compiler flags
  • [ ] Use appropriate data structures
  • [ ] Cache frequently accessed data
  • [ ] Test with realistic data sizes
  • [ ] Measure before and after optimizations

WebAssembly performance optimization is an iterative process. Measure, optimize, measure again. Focus on hot paths and bottlenecks identified through profiling!

Related essays

Next essay
Microservices Architecture: Service Mesh, API Gateway & Patterns