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 OptimizationsPerformance 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!