WebAssembly (WASM)
WebAssembly (WASM) is a binary instruction format that enables near-native performance in web browsers. It serves as a compilation target for languages like C, C++, Rust, and Go, allowing you to run high-performance code alongside JavaScript in the browser.
WASM is not a replacement for JavaScript — it complements it. Use WASM for compute-intensive tasks like image processing, physics simulations, cryptography, video encoding, and game engines. Use JavaScript for DOM manipulation, event handling, and application logic. The two interoperate through a well-defined boundary.
A WebAssembly module is a compact binary format (.wasm) that contains low-level instructions. Modules can be compiled and instantiated in the browser. They run in a sandboxed execution environment with their own linear memory.
| 1 | // Loading and instantiating a WASM module |
| 2 | async function loadWasm() { |
| 3 | const response = await fetch('/module.wasm'); |
| 4 | const bytes = await response.arrayBuffer(); |
| 5 | |
| 6 | // Compile the binary into a module |
| 7 | const module = await WebAssembly.compile(bytes); |
| 8 | |
| 9 | // Instantiate with imports (JavaScript functions the WASM can call) |
| 10 | const instance = await WebAssembly.instantiate(module, { |
| 11 | env: { |
| 12 | memory: new WebAssembly.Memory({ initial: 256 }), // 16MB |
| 13 | log: (value) => console.log('WASM says:', value), |
| 14 | add: (a, b) => a + b, |
| 15 | }, |
| 16 | }); |
| 17 | |
| 18 | // Call exported WASM functions |
| 19 | const result = instance.exports.fibonacci(10); |
| 20 | console.log('fibonacci(10) =', result); // 55 |
| 21 | |
| 22 | return instance; |
| 23 | } |
| 24 | |
| 25 | // Streaming compilation (modern browsers) |
| 26 | async function loadWasmStreaming() { |
| 27 | const { module, instance } = await WebAssembly.instantiateStreaming( |
| 28 | fetch('/module.wasm'), |
| 29 | { |
| 30 | env: { |
| 31 | memory: new WebAssembly.Memory({ initial: 256 }), |
| 32 | }, |
| 33 | } |
| 34 | ); |
| 35 | return instance; |
| 36 | } |
| 37 | |
| 38 | // Check WASM support |
| 39 | const wasmSupported = (() => { |
| 40 | try { |
| 41 | return typeof WebAssembly === 'object' |
| 42 | && typeof WebAssembly.instantiate === 'function'; |
| 43 | } catch { |
| 44 | return false; |
| 45 | } |
| 46 | })(); |
info
Rust is the most popular language for targeting WASM due to its small binary sizes, zero-cost abstractions, and excellent tooling via wasm-pack. The wasm-bindgen crate provides seamless JavaScript interop.
| 1 | // lib.rs — Rust code compiled to WASM |
| 2 | use wasm_bindgen::prelude::*; |
| 3 | |
| 4 | // Expose this function to JavaScript |
| 5 | #[wasm_bindgen] |
| 6 | pub fn fibonacci(n: u32) -> u64 { |
| 7 | match n { |
| 8 | 0 => 0, |
| 9 | 1 => 1, |
| 10 | _ => { |
| 11 | let mut a: u64 = 0; |
| 12 | let mut b: u64 = 1; |
| 13 | for _ in 2..=n { |
| 14 | let temp = b; |
| 15 | b = a + b; |
| 16 | a = temp; |
| 17 | } |
| 18 | b |
| 19 | } |
| 20 | } |
| 21 | } |
| 22 | |
| 23 | // Struct exposed to JavaScript |
| 24 | #[wasm_bindgen] |
| 25 | pub struct ImageProcessor { |
| 26 | width: u32, |
| 27 | height: u32, |
| 28 | data: Vec<u8>, |
| 29 | } |
| 30 | |
| 31 | #[wasm_bindgen] |
| 32 | impl ImageProcessor { |
| 33 | #[wasm_bindgen(constructor)] |
| 34 | pub fn new(width: u32, height: u32, data: &[u8]) -> ImageProcessor { |
| 35 | ImageProcessor { |
| 36 | width, |
| 37 | height, |
| 38 | data: data.to_vec(), |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | pub fn grayscale(&mut self) { |
| 43 | for pixel in self.data.chunks_exact_mut(4) { |
| 44 | let avg = ((pixel[0] as u32 + pixel[1] as u32 + pixel[2] as u32) / 3) as u8; |
| 45 | pixel[0] = avg; |
| 46 | pixel[1] = avg; |
| 47 | pixel[2] = avg; |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | pub fn blur(&mut self, radius: u32) { |
| 52 | let w = self.width as usize; |
| 53 | let h = self.height as usize; |
| 54 | let r = radius as i32; |
| 55 | let mut output = self.data.clone(); |
| 56 | |
| 57 | for y in 0..h { |
| 58 | for x in 0..w { |
| 59 | let mut total_r = 0u32; |
| 60 | let mut total_g = 0u32; |
| 61 | let mut total_b = 0u32; |
| 62 | let mut count = 0u32; |
| 63 | |
| 64 | for dy in -r..=r { |
| 65 | for dx in -r..=r { |
| 66 | let nx = x as i32 + dx; |
| 67 | let ny = y as i32 + dy; |
| 68 | if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 { |
| 69 | let idx = ((ny as usize * w + nx as usize) * 4) as usize; |
| 70 | total_r += self.data[idx] as u32; |
| 71 | total_g += self.data[idx + 1] as u32; |
| 72 | total_b += self.data[idx + 2] as u32; |
| 73 | count += 1; |
| 74 | } |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | let idx = (y * w + x) * 4; |
| 79 | output[idx] = (total_r / count) as u8; |
| 80 | output[idx + 1] = (total_g / count) as u8; |
| 81 | output[idx + 2] = (total_b / count) as u8; |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | self.data = output; |
| 86 | } |
| 87 | |
| 88 | pub fn data_ptr(&self) -> *const u8 { |
| 89 | self.data.as_ptr() |
| 90 | } |
| 91 | |
| 92 | pub fn width(&self) -> u32 { |
| 93 | self.width |
| 94 | } |
| 95 | |
| 96 | pub fn height(&self) -> u32 { |
| 97 | self.height |
| 98 | } |
| 99 | } |
best practice
Emscripten compiles C/C++ code to WASM and generates JavaScript glue code. It provides POSIX-like APIs, OpenGL bindings, and SDL support, making it possible to port existing C/C++ libraries and even full applications to the browser.
| 1 | // image_utils.c — C code compiled to WASM via Emscripten |
| 2 | #include <emscripten.h> |
| 3 | #include <stdlib.h> |
| 4 | #include <string.h> |
| 5 | |
| 6 | EMSCRIPTEN_KEEPALIVE |
| 7 | unsigned char* grayscale(unsigned char* data, int width, int height) { |
| 8 | int size = width * height * 4; |
| 9 | unsigned char* output = (unsigned char*)malloc(size); |
| 10 | for (int i = 0; i < size; i += 4) { |
| 11 | unsigned char avg = (data[i] + data[i+1] + data[i+2]) / 3; |
| 12 | output[i] = avg; |
| 13 | output[i+1] = avg; |
| 14 | output[i+2] = avg; |
| 15 | output[i+3] = data[i+3]; |
| 16 | } |
| 17 | return output; |
| 18 | } |
| 19 | |
| 20 | EMSCRIPTEN_KEEPALIVE |
| 21 | void invert_colors(unsigned char* data, int width, int height) { |
| 22 | int size = width * height * 4; |
| 23 | for (int i = 0; i < size; i += 4) { |
| 24 | data[i] = 255 - data[i]; |
| 25 | data[i+1] = 255 - data[i+1]; |
| 26 | data[i+2] = 255 - data[i+2]; |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | EMSCRIPTEN_KEEPALIVE |
| 31 | float mandelbrot(float cx, float cy, int max_iter) { |
| 32 | float x = 0.0f, y = 0.0f; |
| 33 | int iter = 0; |
| 34 | while (x*x + y*y <= 4.0f && iter < max_iter) { |
| 35 | float xtemp = x*x - y*y + cx; |
| 36 | y = 2*x*y + cy; |
| 37 | x = xtemp; |
| 38 | iter++; |
| 39 | } |
| 40 | return (float)iter / max_iter; |
| 41 | } |
| 1 | // Using the Emscripten-compiled WASM from JavaScript |
| 2 | import createModule from './image_utils.js'; |
| 3 | |
| 4 | async function processImage() { |
| 5 | const Module = await createModule(); |
| 6 | |
| 7 | const canvas = document.getElementById('source-canvas'); |
| 8 | const ctx = canvas.getContext('2d'); |
| 9 | const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); |
| 10 | |
| 11 | const dataPtr = Module._malloc(imageData.data.length); |
| 12 | Module.HEAPU8.set(imageData.data, dataPtr); |
| 13 | |
| 14 | const outputPtr = Module._ccall( |
| 15 | 'grayscale', |
| 16 | 'number', |
| 17 | ['number', 'number', 'number'], |
| 18 | [dataPtr, canvas.width, canvas.height] |
| 19 | ); |
| 20 | |
| 21 | const output = new Uint8ClampedArray( |
| 22 | Module.HEAPU8.buffer, |
| 23 | outputPtr, |
| 24 | imageData.data.length |
| 25 | ); |
| 26 | |
| 27 | const outputCanvas = document.getElementById('output-canvas'); |
| 28 | const outputCtx = outputCanvas.getContext('2d'); |
| 29 | const outputImageData = outputCtx.createImageData(canvas.width, canvas.height); |
| 30 | outputImageData.data.set(output); |
| 31 | outputCtx.putImageData(outputImageData, 0, 0); |
| 32 | |
| 33 | Module._free(dataPtr); |
| 34 | Module._free(outputPtr); |
| 35 | } |
| 36 | |
| 37 | // Using cwrap for cleaner API |
| 38 | async function setupWasmAPI() { |
| 39 | const Module = await createModule(); |
| 40 | |
| 41 | const grayscale = Module.cwrap('grayscale', 'number', [ |
| 42 | 'number', 'number', 'number', |
| 43 | ]); |
| 44 | const invertColors = Module.cwrap('invert_colors', null, [ |
| 45 | 'number', 'number', 'number', |
| 46 | ]); |
| 47 | |
| 48 | return { grayscale, invertColors, _module: Module }; |
| 49 | } |
warning
WebAssembly uses a linear memory model — a contiguous, byte-addressable array of memory. JavaScript and WASM share this memory, enabling efficient data exchange. Memory can grow dynamically but cannot shrink.
| 1 | // Understanding WASM memory |
| 2 | const memory = new WebAssembly.Memory({ |
| 3 | initial: 256, // 256 pages = 16MB (64KB per page) |
| 4 | maximum: 1024, // 64MB max |
| 5 | shared: false, // Set true for SharedArrayBuffer (multithreading) |
| 6 | }); |
| 7 | |
| 8 | // Access the raw memory from JavaScript |
| 9 | const buffer = memory.buffer; |
| 10 | const heap = new Uint8Array(buffer); |
| 11 | const heap32 = new Int32Array(buffer); |
| 12 | |
| 13 | // Write data to WASM memory |
| 14 | function writeToMemory(ptr, data) { |
| 15 | heap.set(data, ptr); |
| 16 | } |
| 17 | |
| 18 | // Read data from WASM memory |
| 19 | function readFromMemory(ptr, length) { |
| 20 | return heap.slice(ptr, ptr + length); |
| 21 | } |
| 22 | |
| 23 | // SharedArrayBuffer for multithreading |
| 24 | const sharedMemory = new WebAssembly.Memory({ |
| 25 | initial: 256, |
| 26 | maximum: 1024, |
| 27 | shared: true, |
| 28 | }); |
| 29 | |
| 30 | // Communication patterns between JS and WASM: |
| 31 | |
| 32 | // Pattern 1: Pass data by copying (small payloads) |
| 33 | // WASM: fn process(data: &[u8]) -> Vec<u8> |
| 34 | // JS: instance.exports.process(new Uint8Array(data)) |
| 35 | |
| 36 | // Pattern 2: Shared memory (large payloads) |
| 37 | // JS writes to shared memory, WASM reads directly |
| 38 | // No copying needed — zero-cost data transfer |
| 39 | |
| 40 | // Pattern 3: Streaming data through memory ring buffer |
| 41 | class RingBuffer { |
| 42 | constructor(memory, capacity) { |
| 43 | this.view = new Uint8Array(memory.buffer); |
| 44 | this.readPos = 0; |
| 45 | this.writePos = 0; |
| 46 | this.capacity = capacity; |
| 47 | } |
| 48 | |
| 49 | write(data) { |
| 50 | for (let i = 0; i < data.length; i++) { |
| 51 | this.view[this.writePos % this.capacity] = data[i]; |
| 52 | this.writePos++; |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | read(length) { |
| 57 | const result = new Uint8Array(length); |
| 58 | for (let i = 0; i < length; i++) { |
| 59 | result[i] = this.view[this.readPos % this.capacity]; |
| 60 | this.readPos++; |
| 61 | } |
| 62 | return result; |
| 63 | } |
| 64 | |
| 65 | get available() { |
| 66 | return this.writePos - this.readPos; |
| 67 | } |
| 68 | } |
pro tip
WASM and JavaScript communicate through imports (JS functions exposed to WASM) and exports (WASM functions callable from JS). The boundary is the key performance consideration — crossing it has overhead.
| 1 | // Full interop example: WASM game of life |
| 2 | // JavaScript provides the rendering, WASM handles the simulation |
| 3 | |
| 4 | const importObject = { |
| 5 | env: { |
| 6 | memory: new WebAssembly.Memory({ initial: 256 }), |
| 7 | |
| 8 | console_log: (ptr, len) => { |
| 9 | const msg = new TextDecoder().decode( |
| 10 | new Uint8Array(memory.buffer, ptr, len) |
| 11 | ); |
| 12 | console.log('[WASM]', msg); |
| 13 | }, |
| 14 | |
| 15 | random: () => Math.random(), |
| 16 | |
| 17 | set_pixel: (x, y, r, g, b) => { |
| 18 | ctx.fillStyle = 'rgb(' + r + ',' + g + ',' + b + ')'; |
| 19 | ctx.fillRect(x, y, 1, 1); |
| 20 | }, |
| 21 | }, |
| 22 | }; |
| 23 | |
| 24 | // Initialize |
| 25 | const { module, instance } = await WebAssembly.instantiateStreaming( |
| 26 | fetch('/game-of.wasm'), |
| 27 | importObject |
| 28 | ); |
| 29 | |
| 30 | const memory = importObject.env.memory; |
| 31 | const gridPtr = instance.exports.init_grid(100, 100); |
| 32 | |
| 33 | // Read grid state from WASM memory |
| 34 | function render() { |
| 35 | const grid = new Uint8Array(memory.buffer, gridPtr, 100 * 100); |
| 36 | |
| 37 | for (let y = 0; y < 100; y++) { |
| 38 | for (let x = 0; x < 100; x++) { |
| 39 | const alive = grid[y * 100 + x]; |
| 40 | ctx.fillStyle = alive ? '#00FF41' : '#0A0A0A'; |
| 41 | ctx.fillRect(x * 6, y * 6, 5, 5); |
| 42 | } |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | // Game loop — minimize JS-WASM boundary crossings |
| 47 | function gameLoop() { |
| 48 | instance.exports.step(); // Single WASM call |
| 49 | render(); |
| 50 | requestAnimationFrame(gameLoop); |
| 51 | } |
| 52 | |
| 53 | gameLoop(); |
| 54 | |
| 55 | // Passing strings between JS and WASM |
| 56 | function passStringToWasm(instance, memory, str) { |
| 57 | const encoder = new TextEncoder(); |
| 58 | const bytes = encoder.encode(str); |
| 59 | const ptr = instance.exports.malloc(bytes.length + 1); |
| 60 | new Uint8Array(memory.buffer, ptr, bytes.length).set(bytes); |
| 61 | new Uint8Array(memory.buffer)[ptr + bytes.length] = 0; // null-terminate |
| 62 | return ptr; |
| 63 | } |
| 64 | |
| 65 | function getStringFromWasm(memory, ptr) { |
| 66 | const bytes = new Uint8Array(memory.buffer); |
| 67 | let end = ptr; |
| 68 | while (bytes[end] !== 0) end++; |
| 69 | return new TextDecoder().decode(bytes.slice(ptr, end)); |
| 70 | } |
warning
WASM excels at compute-intensive tasks where JavaScript falls short. The following are proven use cases where WASM provides significant performance improvements.
| 1 | // Use Case 1: Image/Video Processing |
| 2 | async function processImageWithWasm(imageData) { |
| 3 | const wasm = await loadWasmModule(); |
| 4 | const ptr = wasm.malloc(imageData.data.length); |
| 5 | wasm.HEAPU8.set(imageData.data, ptr); |
| 6 | |
| 7 | wasm.apply_filters(ptr, imageData.width, imageData.height, { |
| 8 | grayscale: true, |
| 9 | blur_radius: 3, |
| 10 | sharpen: 0.5, |
| 11 | contrast: 1.2, |
| 12 | }); |
| 13 | |
| 14 | const result = new Uint8ClampedArray( |
| 15 | wasm.HEAPU8.slice(ptr, ptr + imageData.data.length) |
| 16 | ); |
| 17 | wasm.free(ptr); |
| 18 | return result; |
| 19 | } |
| 20 | |
| 21 | // Use Case 2: Cryptography |
| 22 | async function hashPassword(password, salt) { |
| 23 | const wasm = await loadCryptoWasm(); |
| 24 | const encoder = new TextEncoder(); |
| 25 | const passBytes = encoder.encode(password); |
| 26 | const saltBytes = encoder.encode(salt); |
| 27 | |
| 28 | const passPtr = wasm.malloc(passBytes.length); |
| 29 | const saltPtr = wasm.malloc(saltBytes.length); |
| 30 | const hashPtr = wasm.malloc(32); |
| 31 | |
| 32 | wasm.HEAPU8.set(passBytes, passPtr); |
| 33 | wasm.HEAPU8.set(saltBytes, saltPtr); |
| 34 | wasm.scrypt(passPtr, passBytes.length, saltPtr, saltBytes.length, hashPtr); |
| 35 | |
| 36 | const hash = wasm.HEAPU8.slice(hashPtr, hashPtr + 32); |
| 37 | wasm.free(passPtr); |
| 38 | wasm.free(saltPtr); |
| 39 | wasm.free(hashPtr); |
| 40 | |
| 41 | return Array.from(hash).map(b => b.toString(16).padStart(2, '0')).join(''); |
| 42 | } |
| 43 | |
| 44 | // Use Case 3: Physics Engine |
| 45 | async function createPhysicsEngine() { |
| 46 | const wasm = await loadPhysicsWasm(); |
| 47 | |
| 48 | return { |
| 49 | createWorld: () => wasm.create_world(), |
| 50 | addBody: (world, x, y, mass) => wasm.add_body(world, x, y, mass), |
| 51 | step: (world, dt) => wasm.step(world, dt), |
| 52 | getPosition: (world, id) => { |
| 53 | const ptr = wasm.get_position(world, id); |
| 54 | return { |
| 55 | x: wasm.HEAPF32[ptr >> 2], |
| 56 | y: wasm.HEAPF32[(ptr + 4) >> 2], |
| 57 | }; |
| 58 | }, |
| 59 | }; |
| 60 | } |
| 61 | |
| 62 | // Use Case 4: PDF/Document Parsing |
| 63 | async function parsePDF(arrayBuffer) { |
| 64 | const wasm = await loadPDFWasm(); |
| 65 | const ptr = wasm.malloc(arrayBuffer.byteLength); |
| 66 | wasm.HEAPU8.set(new Uint8Array(arrayBuffer), ptr); |
| 67 | |
| 68 | const pageCount = wasm.pdf_page_count(ptr); |
| 69 | const pages = []; |
| 70 | for (let i = 0; i < pageCount; i++) { |
| 71 | const textPtr = wasm.pdf_extract_text(ptr, i); |
| 72 | pages.push(getStringFromWasm(wasm.memory, textPtr)); |
| 73 | } |
| 74 | |
| 75 | wasm.pdf_free(ptr); |
| 76 | return pages; |
| 77 | } |
| 78 | |
| 79 | // Use Case 5: SQLite in the browser |
| 80 | async function setupInBrowserDatabase() { |
| 81 | const SQL = await initSqlJs(); |
| 82 | const db = new SQL.Database(); |
| 83 | |
| 84 | db.run('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT UNIQUE)'); |
| 85 | db.run("INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')"); |
| 86 | |
| 87 | const results = db.exec('SELECT * FROM users'); |
| 88 | console.log(results[0].values); |
| 89 | } |
best practice
The WASM toolchain includes compilers, package managers, and browser dev tools. Chrome DevTools supports WASM debugging with source maps. Tools like wasm-pack and Emscripten handle the build pipeline.
| 1 | # Rust to WASM with wasm-pack |
| 2 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh |
| 3 | wasm-pack build --target web --out-dir pkg |
| 4 | |
| 5 | # Emscripten for C/C++ |
| 6 | git clone https://github.com/emscripten-core/emsdk.git |
| 7 | cd emsdk |
| 8 | ./emsdk install latest |
| 9 | ./emsdk activate latest |
| 10 | source ./emsdk_env.sh |
| 11 | |
| 12 | # Compile C to WASM |
| 13 | emcc program.c -o program.js \ |
| 14 | -s EXPORTED_FUNCTIONS='["_main","_malloc","_free"]' \ |
| 15 | -s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' \ |
| 16 | -s ALLOW_MEMORY_GROWTH=1 \ |
| 17 | -s MODULARIZE=1 \ |
| 18 | -O3 |
| 19 | |
| 20 | # Compile with WASI (for non-browser environments) |
| 21 | rustup target add wasm32-wasi |
| 22 | cargo build --target wasm32-wasi --release |
| 23 | |
| 24 | # Optimize WASM binary size |
| 25 | wasm-opt -Oz module.wasm -o module.opt.wasm |
| 26 | |
| 27 | # WASM package management |
| 28 | wapm publish # WAPM (WebAssembly Package Manager) |
| 29 | npm publish # wasm-pack generates npm packages |
| 30 | |
| 31 | # Debug in Chrome DevTools: |
| 32 | # 1. Open Sources tab |
| 33 | # 2. Look for WASM source under Scripts |
| 34 | # 3. Set breakpoints in .wat (text format) sources |
| 35 | # 4. Use chrome://flags#enable-webassembly-debugging |
info
- WASM provides near-native performance for compute-intensive browser tasks
- Rust via wasm-pack is the most ergonomic toolchain for new WASM projects
- Emscripten lets you port existing C/C++ codebases to the browser
- WASM linear memory is shared with JavaScript — use it for zero-copy data exchange
- Minimize JS-WASM boundary crossings — batch operations into single calls
- Real-world use cases: image processing, crypto, physics, SQLite, PDF parsing
- Chrome DevTools supports WASM debugging with DWARF source maps