WebAssembly Complete Guide
Introduction
WebAssembly (WASM) is a binary instruction format that runs at near-native speed in browsers. This guide covers WASM fundamentals, compilation from C/C++/Rust, JavaScript integration, memory management, threading, and practical use cases for performance-critical web applications.
1. WebAssembly Fundamentals
# What is WebAssembly?
- **Binary format** for stack-based virtual machine
- **Near-native performance** (typically 50-80% of native speed)
- **Language-agnostic** (C, C++, Rust, Go, etc.)
- **Runs alongside JavaScript** in browsers
- **Safe and sandboxed** execution environment
# Key Features
1. **Fast**: Compiled to optimized machine code
2. **Safe**: Memory-safe, sandboxed execution
3. **Open**: Designed as open standard
4. **Portable**: Runs on any platform with WASM runtime
5. **Compact**: Binary format is smaller than JavaScript
# Use Cases
- **Compute-intensive tasks**: Image/video processing, compression
- **Games**: 3D engines, physics simulations
- **Scientific computing**: Data analysis, machine learning
- **Cryptography**: Encryption, hashing algorithms
- **Legacy code**: Port existing C/C++ libraries to web
- **Performance-critical**: Audio/video codecs, parsers
# WASM vs JavaScript Performance
Task JavaScript WebAssembly
Image processing 100ms 20ms (5x faster)
Matrix multiplication 500ms 80ms (6x faster)
Fibonacci (recursive) 200ms 150ms (1.3x faster)
Compression 1000ms 200ms (5x faster)
2. Getting Started with Emscripten (C/C++)
# Install Emscripten
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
# Simple C program
// hello.c
#include <stdio.h>
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
return a + b;
}
EMSCRIPTEN_KEEPALIVE
void greet(const char* name) {
printf("Hello, %s!\n", name);
}
int main() {
printf("WebAssembly module loaded\n");
return 0;
}
# Compile to WebAssembly
emcc hello.c -o hello.js -s EXPORTED_FUNCTIONS='["_add", "_greet"]' -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]'
# This generates:
# - hello.wasm (binary module)
# - hello.js (JavaScript glue code)
# Load in HTML
<!DOCTYPE html>
<html>
<head>
<title>WebAssembly Demo</title>
</head>
<body>
<script src="hello.js"></script>
<script>
Module.onRuntimeInitialized = () => {
// Call exported functions
const add = Module.cwrap('add', 'number', ['number', 'number']);
const result = add(5, 3);
console.log('5 + 3 =', result);
// Call function with string
Module.ccall('greet', null, ['string'], ['World']);
};
</script>
</body>
</html>
# Image processing example
// blur.c
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
void blur(unsigned char* data, int width, int height) {
unsigned char* temp = (unsigned char*)malloc(width * height * 4);
for (int y = 1; y < height - 1; y++) {
for (int x = 1; x < width - 1; x++) {
int idx = (y * width + x) * 4;
for (int c = 0; c < 3; c++) {
int sum = 0;
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
int i = ((y + dy) * width + (x + dx)) * 4 + c;
sum += data[i];
}
}
temp[idx + c] = sum / 9;
}
temp[idx + 3] = data[idx + 3]; // Alpha channel
}
}
memcpy(data, temp, width * height * 4);
free(temp);
}
# Compile
emcc blur.c -o blur.js -s EXPORTED_FUNCTIONS='["_blur"]' -s ALLOW_MEMORY_GROWTH=1
3. Rust and WebAssembly
# Install wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# Create new Rust WASM project
cargo new --lib my_wasm_lib
cd my_wasm_lib
# Cargo.toml
[package]
name = "my_wasm_lib"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
# src/lib.rs
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u64 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
#[wasm_bindgen]
pub struct Counter {
value: i32,
}
#[wasm_bindgen]
impl Counter {
#[wasm_bindgen(constructor)]
pub fn new() -> Counter {
Counter { value: 0 }
}
pub fn increment(&mut self) {
self.value += 1;
}
pub fn get_value(&self) -> i32 {
self.value
}
}
// Work with JavaScript types
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
// Build for web
wasm-pack build --target web
# This generates pkg/ directory with:
# - my_wasm_lib_bg.wasm
# - my_wasm_lib.js
# - my_wasm_lib.d.ts
# Use in HTML
<script type="module">
import init, { add, fibonacci, Counter, greet } from './pkg/my_wasm_lib.js';
async function run() {
await init();
console.log('5 + 3 =', add(5, 3));
console.log('Fibonacci(10) =', fibonacci(10));
console.log(greet('World'));
const counter = new Counter();
counter.increment();
console.log('Counter:', counter.get_value());
}
run();
</script>
# Image manipulation example
use wasm_bindgen::prelude::*;
use wasm_bindgen::Clamped;
use web_sys::ImageData;
#[wasm_bindgen]
pub fn grayscale(data: &mut [u8]) {
for chunk in data.chunks_exact_mut(4) {
let avg = (chunk[0] as u32 + chunk[1] as u32 + chunk[2] as u32) / 3;
chunk[0] = avg as u8;
chunk[1] = avg as u8;
chunk[2] = avg as u8;
}
}
#[wasm_bindgen]
pub fn invert(data: &mut [u8]) {
for i in (0..data.len()).step_by(4) {
data[i] = 255 - data[i]; // R
data[i + 1] = 255 - data[i + 1]; // G
data[i + 2] = 255 - data[i + 2]; // B
}
}
4. JavaScript Integration
// Load WASM module directly
async function loadWASM() {
const response = await fetch('module.wasm');
const buffer = await response.arrayBuffer();
const module = await WebAssembly.compile(buffer);
const instance = await WebAssembly.instantiate(module);
return instance.exports;
}
// Or use shorthand
async function loadWASMShort() {
const { instance } = await WebAssembly.instantiateStreaming(
fetch('module.wasm')
);
return instance.exports;
}
// Use the module
const wasm = await loadWASM();
const result = wasm.add(5, 3);
console.log(result);
// Pass memory between JS and WASM
const memory = new WebAssembly.Memory({ initial: 1 }); // 1 page = 64KB
const { instance } = await WebAssembly.instantiate(wasmModule, {
env: {
memory: memory,
abort: () => console.error('WASM aborted')
}
});
// Write data to WASM memory
const data = new Uint8Array(memory.buffer);
data[0] = 42;
data[1] = 100;
// Read from WASM memory
const result = new Uint32Array(memory.buffer, 0, 1);
console.log('Result:', result[0]);
// React component with WASM
import { useEffect, useState } from 'react';
function ImageProcessor() {
const [wasm, setWasm] = useState(null);
const [processing, setProcessing] = useState(false);
useEffect(() => {
loadWASM().then(setWasm);
}, []);
const processImage = async (imageFile) => {
if (!wasm) return;
setProcessing(true);
const img = await loadImage(imageFile);
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Process with WASM
wasm.grayscale(imageData.data);
ctx.putImageData(imageData, 0, 0);
setProcessing(false);
return canvas.toDataURL();
};
return (
<div>
<input
type="file"
accept="image/*"
onChange={(e) => processImage(e.target.files[0])}
disabled={!wasm || processing}
/>
{processing && <p>Processing...</p>}
</div>
);
}
// Performance comparison
async function benchmark() {
const size = 1000000;
const data = new Float32Array(size);
for (let i = 0; i < size; i++) {
data[i] = Math.random();
}
// JavaScript implementation
console.time('JS');
let jsSum = 0;
for (let i = 0; i < data.length; i++) {
jsSum += data[i] * data[i];
}
console.timeEnd('JS');
// WebAssembly implementation
console.time('WASM');
const wasm = await loadWASM();
const wasmSum = wasm.sumSquares(data.length);
console.timeEnd('WASM');
console.log('JS:', jsSum, 'WASM:', wasmSum);
}
5. Memory Management
// WASM memory is linear array of bytes
const memory = new WebAssembly.Memory({
initial: 10, // 10 pages = 640KB
maximum: 100 // 100 pages = 6.4MB
});
// Grow memory dynamically
memory.grow(5); // Add 5 pages (320KB)
// Access memory from JavaScript
const buffer = memory.buffer;
const uint8View = new Uint8Array(buffer);
const uint32View = new Uint32Array(buffer);
const float64View = new Float64Array(buffer);
// Rust example with manual memory management
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn allocate(size: usize) -> *mut u8 {
let mut buf = Vec::with_capacity(size);
let ptr = buf.as_mut_ptr();
std::mem::forget(buf);
ptr
}
#[wasm_bindgen]
pub fn deallocate(ptr: *mut u8, size: usize) {
unsafe {
let _ = Vec::from_raw_parts(ptr, 0, size);
}
}
// JavaScript usage
const size = 1024;
const ptr = wasm.allocate(size);
// Use memory at ptr
const view = new Uint8Array(wasm.memory.buffer, ptr, size);
view[0] = 42;
// Free memory when done
wasm.deallocate(ptr, size);
// Better approach: Use Vec and return ownership
#[wasm_bindgen]
pub fn process_data(input: Vec<u8>) -> Vec<u8> {
// Process data
let mut output = input;
for byte in &mut output {
*byte = byte.wrapping_add(1);
}
output
}
// JavaScript
const input = new Uint8Array([1, 2, 3, 4, 5]);
const output = wasm.process_data(input);
console.log(output); // [2, 3, 4, 5, 6]
6. Threading and SIMD
// WebAssembly threads (requires SharedArrayBuffer)
// Note: Requires specific headers for cross-origin isolation
// C code with pthreads
#include <pthread.h>
#include <emscripten.h>
void* worker_thread(void* arg) {
int* value = (int*)arg;
*value = *value * 2;
return NULL;
}
EMSCRIPTEN_KEEPALIVE
int parallel_compute(int n) {
pthread_t threads[4];
int values[4] = {n, n, n, n};
for (int i = 0; i < 4; i++) {
pthread_create(&threads[i], NULL, worker_thread, &values[i]);
}
for (int i = 0; i < 4; i++) {
pthread_join(threads[i], NULL);
}
int sum = 0;
for (int i = 0; i < 4; i++) {
sum += values[i];
}
return sum;
}
# Compile with threading
emcc code.c -o output.js -pthread -s PTHREAD_POOL_SIZE=4
// SIMD (Single Instruction Multiple Data)
// Rust with SIMD
use std::arch::wasm32::*;
#[wasm_bindgen]
pub fn add_arrays_simd(a: &[f32], b: &[f32]) -> Vec<f32> {
let mut result = Vec::with_capacity(a.len());
unsafe {
for i in (0..a.len()).step_by(4) {
let va = v128_load(a.as_ptr().add(i) as *const v128);
let vb = v128_load(b.as_ptr().add(i) as *const v128);
let vr = f32x4_add(va, vb);
let mut temp = [0f32; 4];
v128_store(temp.as_mut_ptr() as *mut v128, vr);
result.extend_from_slice(&temp);
}
}
result
}
# Compile with SIMD support
wasm-pack build --target web -- --features simd
// Web Workers for parallel WASM execution
// main.js
const worker = new Worker('wasm-worker.js');
worker.postMessage({
type: 'process',
data: largeDataArray
});
worker.onmessage = (e) => {
console.log('Result:', e.data.result);
};
// wasm-worker.js
importScripts('module.js');
let wasm;
Module.onRuntimeInitialized = () => {
wasm = Module;
self.postMessage({ type: 'ready' });
};
self.onmessage = (e) => {
if (e.data.type === 'process') {
const result = wasm.process(e.data.data);
self.postMessage({ type: 'result', result });
}
};
7. Real-World Examples
// Example 1: Video compression
// Rust implementation
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct VideoEncoder {
width: u32,
height: u32,
}
#[wasm_bindgen]
impl VideoEncoder {
#[wasm_bindgen(constructor)]
pub fn new(width: u32, height: u32) -> VideoEncoder {
VideoEncoder { width, height }
}
pub fn encode_frame(&self, frame: &[u8]) -> Vec<u8> {
// H.264 encoding logic (simplified)
let mut compressed = Vec::new();
// Apply DCT transform
// Quantization
// Entropy coding
compressed
}
}
// JavaScript usage
import init, { VideoEncoder } from './pkg/video_encoder.js';
async function encodeVideo(videoElement) {
await init();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
const encoder = new VideoEncoder(canvas.width, canvas.height);
const frames = [];
const captureFrame = () => {
ctx.drawImage(videoElement, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const compressed = encoder.encode_frame(imageData.data);
frames.push(compressed);
};
videoElement.addEventListener('timeupdate', captureFrame);
}
// Example 2: Cryptography
use wasm_bindgen::prelude::*;
use sha2::{Sha256, Digest};
use aes::Aes256;
use block_modes::{BlockMode, Cbc};
use block_modes::block_padding::Pkcs7;
type Aes256Cbc = Cbc<Aes256, Pkcs7>;
#[wasm_bindgen]
pub fn hash_sha256(data: &[u8]) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(data);
hasher.finalize().to_vec()
}
#[wasm_bindgen]
pub fn encrypt_aes(key: &[u8], iv: &[u8], plaintext: &[u8]) -> Vec<u8> {
let cipher = Aes256Cbc::new_from_slices(key, iv).unwrap();
cipher.encrypt_vec(plaintext)
}
// Example 3: Game physics
#[wasm_bindgen]
pub struct PhysicsWorld {
bodies: Vec<RigidBody>,
gravity: f32,
}
#[wasm_bindgen]
impl PhysicsWorld {
pub fn step(&mut self, dt: f32) {
for body in &mut self.bodies {
body.velocity.y += self.gravity * dt;
body.position.x += body.velocity.x * dt;
body.position.y += body.velocity.y * dt;
}
self.check_collisions();
}
fn check_collisions(&mut self) {
// Collision detection and response
}
}
8. Best Practices
โ WebAssembly Best Practices:
- โ Use WASM for compute-intensive tasks only
- โ Minimize data transfer between JS and WASM
- โ Reuse WASM memory allocations when possible
- โ Use streaming compilation for large modules
- โ Profile before and after WASM optimization
- โ Leverage SIMD for parallel data processing
- โ Use Web Workers for CPU-intensive WASM tasks
- โ Enable compiler optimizations (-O3 for emcc)
- โ Cache compiled WASM modules
- โ Handle WASM errors gracefully in JavaScript
- โ Use modern WASM features (threads, SIMD) when supported
- โ Test across different browsers and devices
- โ Monitor WASM module size (compress with gzip/brotli)
- โ Document WASM-JS interface clearly
- โ Provide JavaScript fallback for older browsers
Conclusion
WebAssembly brings near-native performance to web applications. Use it for compute-intensive tasks like image processing, games, cryptography, and scientific computing. Compile from C/C++/Rust using Emscripten or wasm-pack, integrate seamlessly with JavaScript, and manage memory carefully. WASM is production-ready and used by major applications like Figma, Google Earth, and AutoCAD Web.
๐ก Pro Tip: Start with Rust and wasm-bindgen for the best developer experienceโit provides excellent type safety, memory management, and integration with JavaScript. Use wasm-pack for easy builds and npm publishing. For existing C/C++ codebases, Emscripten works great but requires more manual memory management. Always benchmarkโWASM shines for computational tasks but has overhead for small operations. Consider using AssemblyScript (TypeScript-like syntax) for easier learning. Monitor performance with Chrome DevTools WASM profiler and optimize hot paths with SIMD instructions.