|$ curl https://forge-ai.dev/api/markdown?path=docs/web-platform/webassembly
$cat docs/webassembly-(wasm).md
updated Recently·35 min read·published

WebAssembly (WASM)

WebAssemblyWASMPerformanceAdvanced🎯Free Tools
Introduction

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.

WASM Basics

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.

wasm-basics.js
JavaScript
1// Loading and instantiating a WASM module
2async 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)
26async 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
39const wasmSupported = (() => {
40 try {
41 return typeof WebAssembly === 'object'
42 && typeof WebAssembly.instantiate === 'function';
43 } catch {
44 return false;
45 }
46})();

info

Use WebAssembly.instantiateStreaming() instead of fetch + compile + instantiate. It streams and compiles the .wasm file in one step, which is significantly faster for large modules. It also works with Content-Encoding: gzip so you can serve pre-compressed WASM files.
Rust to WASM

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.

lib.rs
RUST
1// lib.rs — Rust code compiled to WASM
2use wasm_bindgen::prelude::*;
3
4// Expose this function to JavaScript
5#[wasm_bindgen]
6pub 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]
25pub struct ImageProcessor {
26 width: u32,
27 height: u32,
28 data: Vec<u8>,
29}
30
31#[wasm_bindgen]
32impl 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

Run wasm-pack build --target web to generate a JS/TS package that can be imported directly. wasm-pack handles the entire build pipeline: compiles Rust to WASM, generates JS bindings, and produces an npm-publishable package.
C/C++ to WASM with Emscripten

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.

image_utils.c
C
1// image_utils.c — C code compiled to WASM via Emscripten
2#include <emscripten.h>
3#include <stdlib.h>
4#include <string.h>
5
6EMSCRIPTEN_KEEPALIVE
7unsigned 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
20EMSCRIPTEN_KEEPALIVE
21void 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
30EMSCRIPTEN_KEEPALIVE
31float 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}
wasm-usage.js
JavaScript
1// Using the Emscripten-compiled WASM from JavaScript
2import createModule from './image_utils.js';
3
4async 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
38async 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

Memory management is critical when using WASM. Every malloc() must have a corresponding free() to avoid memory leaks. Use Module.HEAPU8 to read/write WASM linear memory directly, but be careful with bounds — WASM can only access its own memory region.
WASM Memory Model

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.

wasm-memory.js
JavaScript
1// Understanding WASM memory
2const 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
9const buffer = memory.buffer;
10const heap = new Uint8Array(buffer);
11const heap32 = new Int32Array(buffer);
12
13// Write data to WASM memory
14function writeToMemory(ptr, data) {
15 heap.set(data, ptr);
16}
17
18// Read data from WASM memory
19function readFromMemory(ptr, length) {
20 return heap.slice(ptr, ptr + length);
21}
22
23// SharedArrayBuffer for multithreading
24const 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
41class 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

For large datasets (images, audio, video), use shared memory to avoid copying data between JavaScript and WASM. The WASM module reads directly from the same ArrayBuffer that JavaScript writes to — zero overhead. Set shared: true and use SharedArrayBuffer for this.
JavaScript Interop

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.

wasm-interop.js
JavaScript
1// Full interop example: WASM game of life
2// JavaScript provides the rendering, WASM handles the simulation
3
4const 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
25const { module, instance } = await WebAssembly.instantiateStreaming(
26 fetch('/game-of.wasm'),
27 importObject
28);
29
30const memory = importObject.env.memory;
31const gridPtr = instance.exports.init_grid(100, 100);
32
33// Read grid state from WASM memory
34function 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
47function gameLoop() {
48 instance.exports.step(); // Single WASM call
49 render();
50 requestAnimationFrame(gameLoop);
51}
52
53gameLoop();
54
55// Passing strings between JS and WASM
56function 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
65function 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

Every call across the JS-WASM boundary has overhead. Batch operations into single WASM calls whenever possible instead of making many small calls. For example, process an entire image array in one WASM function call instead of calling a WASM function for each pixel.
Use Cases

WASM excels at compute-intensive tasks where JavaScript falls short. The following are proven use cases where WASM provides significant performance improvements.

wasm-use-cases.js
JavaScript
1// Use Case 1: Image/Video Processing
2async 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
22async 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
45async 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
63async 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
80async 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

Use WASM when JavaScript performance is insufficient for the task — typically compute-bound operations like math, image processing, or parsing. For I/O-bound work, DOM manipulation, or simple business logic, JavaScript is faster to develop and performant enough.
Tooling & Debugging

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.

tooling.sh
Bash
1# Rust to WASM with wasm-pack
2curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
3wasm-pack build --target web --out-dir pkg
4
5# Emscripten for C/C++
6git clone https://github.com/emscripten-core/emsdk.git
7cd emsdk
8./emsdk install latest
9./emsdk activate latest
10source ./emsdk_env.sh
11
12# Compile C to WASM
13emcc 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)
21rustup target add wasm32-wasi
22cargo build --target wasm32-wasi --release
23
24# Optimize WASM binary size
25wasm-opt -Oz module.wasm -o module.opt.wasm
26
27# WASM package management
28wapm publish # WAPM (WebAssembly Package Manager)
29npm 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

Chrome DevTools supports setting breakpoints in WASM source code when you include DWARF debug information. Compile with -g flag to include debug symbols. The Sources tab will show your original source files with full breakpoint support.
Key Takeaways
  • 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