WebAssembly in Practice: High-Performance Code in the Browser
WebAssembly (Wasm) is a binary instruction format that runs in modern browsers at near-native speed. It lets you compile C, C++, Rust, Go, and other languages to a portable binary that runs alongside JavaScript.
Why WebAssembly?
- Performance: Runs at near-native speed, much faster than JS for compute-heavy tasks
- Portability: Same binary runs in any browser or Node.js
- Language flexibility: Use Rust, C++ where they excel
- Security: Runs in a sandboxed environment
Compiling Rust to WebAssembly
# Install wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# Create a new Rust + Wasm project
wasm-pack new hello-wasm
cd hello-wasm
// src/lib.rs
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}! From WebAssembly.", name)
}
# Build for web
wasm-pack build --target web
Using Wasm in JavaScript
import init, { fibonacci, greet } from './pkg/hello_wasm.js';
async function run() {
await init();
console.log(greet("World")); // "Hello, World! From WebAssembly."
const start = performance.now();
const result = fibonacci(40);
const end = performance.now();
console.log(`fibonacci(40) = ${result}, took ${end - start}ms`);
}
run();
Memory Management
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct ImageProcessor {
width: u32,
height: u32,
data: Vec<u8>,
}
#[wasm_bindgen]
impl ImageProcessor {
#[wasm_bindgen(constructor)]
pub fn new(width: u32, height: u32) -> ImageProcessor {
ImageProcessor {
width,
height,
data: vec![0; (width * height * 4) as usize],
}
}
pub fn grayscale(&mut self) {
let pixels = self.data.chunks_mut(4);
for pixel in pixels {
let gray = (0.299 * pixel[0] as f32
+ 0.587 * pixel[1] as f32
+ 0.114 * pixel[2] as f32) as u8;
pixel[0] = gray;
pixel[1] = gray;
pixel[2] = gray;
}
}
pub fn data_ptr(&self) -> *const u8 {
self.data.as_ptr()
}
}
Performance Best Practices
// 1. Minimize JS-Wasm boundary crossings
// BAD: Crossing boundary in a loop
for (let i = 0; i < 1000000; i++) {
wasmModule.process_single_item(i);
}
// GOOD: Pass bulk data to Wasm
wasmModule.process_all_items(dataArray, 1000000);
// 2. Streaming compilation for large Wasm files
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/module.wasm'),
importObject
);
WASI: Wasm Outside the Browser
// WASI allows Wasm to run as a server-side runtime
fn main() {
println!("Hello from WASI!");
let content = std::fs::read_to_string("input.txt").unwrap();
println!("File content: {}", content);
}
# Run with wasmtime
cargo build --target wasm32-wasi
wasmtime target/wasm32-wasi/debug/my-app.wasm
Real-World Use Cases
- Video/Image Processing: ffmpeg.wasm for client-side video conversion
- Cryptography: Fast crypto operations (bcrypt, AES)
- Game Engines: Quake, Doom running in browser
- Scientific Computing: Python (Pyodide) in browser
- SQLite: sql.js for client-side database
Summary
WebAssembly enables high-performance code in the browser. Key takeaways:
- Use Rust or C++ for CPU-intensive work
- Minimize JS/Wasm boundary crossings
- Work with typed arrays for efficient memory sharing
- Consider WASI for portable server-side Wasm