Skip to content
All essays
WebMarch 27, 202614 min

WebAssembly Performance: Optimization Techniques

Master WebAssembly performance optimization and build lightning-fast WASM applications

Ü
Ümit Uz
Mobile & Full Stack Developer

WebAssembly delivers near-native performance, but proper optimization is crucial for maximum speed.

Performance Principles

  1. 1Minimize JavaScript-WASM boundary crossings
  2. 2Use linear memory efficiently
  3. 3Optimize memory allocations
  4. 4Leverage SIMD when available
  5. 5Profile and optimize hot paths

Memory Optimization

Efficient Memory Layout

rust
// Efficient data structures for WASM
#[repr(C)]
struct Particle {
    x: f32,
    y: f32,
    vx: f32,
    vy: f32,
}

// Array of Structs (AoS) - good for random access
struct ParticleSystemAoS {
    particles: Vec<Particle>,
}

// Struct of Arrays (SoA) - better for SIMD
struct ParticleSystemSoA {
    x: Vec<f32>,
    y: Vec<f32>,
    vx: Vec<f32>,
    vy: Vec<f32>,
}

// Use arena allocator to reduce fragmentation
struct Arena<T> {
    data: Vec<T>,
    free_list: Vec<usize>,
}

impl<T> Arena<T> {
    fn allocate(&mut self) -> usize {
        if let Some(idx) = self.free_list.pop() {
            idx
        } else {
            let idx = self.data.len();
            self.data.push(unsafe { std::mem::zeroed() });
            idx
        }
    }
}

Memory Pool Pattern

rust
// Object pooling to reduce allocations
struct MemoryPool<T> {
    objects: Vec<Option<T>>,
    free: Vec<usize>,
}

impl<T: Default> MemoryPool<T> {
    fn new(capacity: usize) -> Self {
        let mut objects = Vec::with_capacity(capacity);
        let mut free = Vec::with_capacity(capacity);
        
        for i in 0..capacity {
            objects.push(Some(T::default()));
            free.push(i);
        }
        
        Self { objects, free }
    }
    
    fn acquire(&mut self) -> Option<usize> {
        self.free.pop().map(|idx| {
            self.objects[idx].take();
            idx
        })
    }
    
    fn release(&mut self, idx: usize, item: T) {
        self.objects[idx] = Some(item);
        self.free.push(idx);
    }
}

SIMD Optimization

rust
// SIMD-accelerated vector operations
#[cfg(target_arch = "wasm32")]
use std::arch::wasm32::*;

// Vector addition with SIMD
pub fn vector_add(a: &[f32], b: &[f32], result: &mut [f32]) {
    assert!(a.len() == b.len() && a.len() == result.len());
    
    // Process 4 elements at a time
    let chunks = a.len() / 4;
    
    for i in 0..chunks {
        unsafe {
            let a_vec = v128_load(a.as_ptr().add(i * 4) as *const v128);
            let b_vec = v128_load(b.as_ptr().add(i * 4) as *const v128);
            let result_vec = f32x4_add(a_vec, b_vec);
            v128_store(result.as_mut_ptr().add(i * 4) as *mut v128, result_vec);
        }
    }
    
    // Handle remaining elements
    for i in (chunks * 4)..a.len() {
        result[i] = a[i] + b[i];
    }
}

JavaScript Interop Optimization

Batch Operations

typescript
// Efficient JS-WASM communication
class WASMPhysicsEngine {
  private instance: WebAssembly.Instance;
  private memory: WebAssembly.Memory;
  
  constructor(instance: WebAssembly.Instance) {
    this.instance = instance;
    this.memory = (instance.exports.memory as WebAssembly.Memory);
  }
  
  // Bad: Multiple boundary crossings
  updateBad(particles: Particle[]) {
    for (const p of particles) {
      this.instance.exports.update_particle(p.x, p.y, p.vx, p.vy);
    }
  }
  
  // Good: Batch operations
  updateGood(particles: Particle[]) {
    // Allocate array in WASM memory
    const ptr = this.instance.exports.alloc_particles(particles.length);
    
    // Copy all particles at once
    const buffer = new Float32Array(this.memory.buffer, ptr, particles.length * 4);
    particles.forEach((p, i) => {
      buffer[i * 4 + 0] = p.x;
      buffer[i * 4 + 1] = p.y;
      buffer[i * 4 + 2] = p.vx;
      buffer[i * 4 + 3] = p.vy;
    });
    
    // Single call to update all particles
    this.instance.exports.update_particles(ptr, particles.length);
    
    // Free memory
    this.instance.exports.free_particles(ptr, particles.length);
  }
}

Parallel Processing

rust
// Parallel processing with Web Workers
use wasm_bindgen::prelude::*;
use web_sys::Worker;

#[wasm_bindgen]
pub struct ParallelProcessor {
    workers: Vec<Worker>,
}

#[wasm_bindgen]
impl ParallelProcessor {
    #[wasm_bindgen(constructor)]
    pub fn new(num_workers: usize) -> Self {
        let mut workers = Vec::with_capacity(num_workers);
        
        for _ in 0..num_workers {
            let worker = Worker::new("/worker.js").unwrap();
            workers.push(worker);
        }
        
        Self { workers }
    }
    
    pub fn process_parallel(&self, data: Vec<f32>) -> JsValue {
        let chunk_size = data.len() / self.workers.len();
        let results = Vec::new();
        
        for (i, worker) in self.workers.iter().enumerate() {
            let start = i * chunk_size;
            let end = if i == self.workers.len() - 1 {
                data.len()
            } else {
                start + chunk_size
            };
            
            let chunk = &data[start..end];
            worker.post_message(&chunk.into());
        }
        
        // Collect results from workers
        JsValue::from_serde(&results).unwrap()
    }
}

Profiling

typescript
// Performance profiling
class WASMProfiler {
  private metrics = new Map<string, number[]>();
  
  measure(name: string, fn: () => void) {
    const start = performance.now();
    fn();
    const duration = performance.now() - start;
    
    if (!this.metrics.has(name)) {
      this.metrics.set(name, []);
    }
    
    this.metrics.get(name)!.push(duration);
  }
  
  getStats(name: string) {
    const times = this.metrics.get(name) || [];
    
    return {
      count: times.length,
      min: Math.min(...times),
      max: Math.max(...times),
      avg: times.reduce((a, b) => a + b, 0) / times.length,
      median: times.sort((a, b) => a - b)[Math.floor(times.length / 2)]
    };
  }
  
  report() {
    for (const [name, _] of this.metrics) {
      console.log(name, this.getStats(name));
    }
  }
}

Best Practices

  1. 1Profile before optimizing
  2. 2Minimize JS-WASM boundary crossings
  3. 3Use efficient memory layouts
  4. 4Leverage SIMD for parallelizable work
  5. 5Pool memory allocations
  6. 6Batch operations when possible
  7. 7Use Web Workers for parallelism

Conclusion

WASM performance requires understanding both the host and guest environments. Profile carefully, optimize hot paths, and measure the impact of every optimization.

Enjoyed this essay? Share it.

Related essays

Next essay
Cross-Platform Mobile Development: Framework Comparison