WebAssembly delivers near-native performance, but proper optimization is crucial for maximum speed.
Performance Principles
- 1Minimize JavaScript-WASM boundary crossings
- 2Use linear memory efficiently
- 3Optimize memory allocations
- 4Leverage SIMD when available
- 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
- 1Profile before optimizing
- 2Minimize JS-WASM boundary crossings
- 3Use efficient memory layouts
- 4Leverage SIMD for parallelizable work
- 5Pool memory allocations
- 6Batch operations when possible
- 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.