WASM in Browser
Using WebAssembly in the browser for canvas rendering, audio processing, Web Workers, and DOM integration
You are an expert in browser-side WebAssembly usage for building WebAssembly applications.
## Key Points
1. **Fetch** the `.wasm` binary (typically via `fetch()`)
2. **Compile** the binary to a `WebAssembly.Module`
3. **Instantiate** with imports to get a `WebAssembly.Instance`
4. **Call** exported functions
- **Canvas 2D/WebGL/WebGPU** — Wasm computes geometry/pixels, JS submits draw calls
- **Web Audio** — Wasm generates audio samples in an `AudioWorkletProcessor`
- **Web Workers** — Wasm runs in worker threads for parallel computation
- **Fetch/XHR** — JS fetches data and writes it into Wasm memory
- **IndexedDB/localStorage** — JS handles persistence, Wasm processes data
- **Use `instantiateStreaming`** — it compiles while downloading and is the fastest way to load Wasm in the browser. Ensure the server sends `Content-Type: application/wasm`.
- **Offload heavy computation to Web Workers** — Wasm computation on the main thread blocks the UI. Use workers for any task taking more than 16ms.
- **Cache compiled modules** — `WebAssembly.Module` supports structured cloning. Store compiled modules in IndexedDB to skip recompilation on subsequent page loads.
## Quick Example
```javascript
// main.js
const audioCtx = new AudioContext();
await audioCtx.audioWorklet.addModule("wasm-audio-processor.js");
const node = new AudioWorkletNode(audioCtx, "wasm-synth");
node.connect(audioCtx.destination);
```
```html
<!-- CSP header must allow wasm-unsafe-eval for Wasm compilation -->
<meta http-equiv="Content-Security-Policy"
content="script-src 'self' 'wasm-unsafe-eval'">
```skilldb get webassembly-skills/WASM in BrowserFull skill: 324 linesBrowser-Side Wasm — WebAssembly
You are an expert in browser-side WebAssembly usage for building WebAssembly applications.
Overview
Browsers were the original execution environment for WebAssembly. All major browsers (Chrome, Firefox, Safari, Edge) support Wasm with near-native performance. Browser-side Wasm is used for computationally intensive tasks like image processing, game engines, audio synthesis, physics simulations, and video codecs, while delegating UI and DOM manipulation to JavaScript.
Core Concepts
Loading Pipeline
- Fetch the
.wasmbinary (typically viafetch()) - Compile the binary to a
WebAssembly.Module - Instantiate with imports to get a
WebAssembly.Instance - Call exported functions
Streaming compilation (instantiateStreaming) merges steps 1-3, compiling while downloading.
Browser APIs Available to Wasm
Wasm cannot directly call browser APIs. It must go through JavaScript imports. Common patterns:
- Canvas 2D/WebGL/WebGPU — Wasm computes geometry/pixels, JS submits draw calls
- Web Audio — Wasm generates audio samples in an
AudioWorkletProcessor - Web Workers — Wasm runs in worker threads for parallel computation
- Fetch/XHR — JS fetches data and writes it into Wasm memory
- IndexedDB/localStorage — JS handles persistence, Wasm processes data
SIMD Support
All modern browsers support the Wasm SIMD proposal (v128 type). This enables 4x f32 or 2x f64 parallel operations, critical for image processing and physics.
Implementation Patterns
Minimal HTML Loading
<!DOCTYPE html>
<html>
<head><title>Wasm App</title></head>
<body>
<canvas id="canvas" width="800" height="600"></canvas>
<script type="module">
const { instance } = await WebAssembly.instantiateStreaming(
fetch("app.wasm"),
{ env: { /* imports */ } }
);
instance.exports.init();
requestAnimationFrame(function loop() {
instance.exports.update();
requestAnimationFrame(loop);
});
</script>
</body>
</html>
Canvas 2D Rendering
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const width = canvas.width;
const height = canvas.height;
const { instance } = await WebAssembly.instantiateStreaming(
fetch("renderer.wasm"),
{}
);
const memory = instance.exports.memory;
const render = instance.exports.render;
const bufferPtr = instance.exports.get_buffer_ptr();
function draw() {
// Wasm writes RGBA pixel data into its memory
render(width, height, performance.now());
// Create an ImageData view over Wasm memory
const pixels = new Uint8ClampedArray(
memory.buffer,
bufferPtr,
width * height * 4
);
const imageData = new ImageData(pixels, width, height);
ctx.putImageData(imageData, 0, 0);
requestAnimationFrame(draw);
}
draw();
WebGL Integration
const canvas = document.getElementById("gl-canvas");
const gl = canvas.getContext("webgl2");
const { instance } = await WebAssembly.instantiateStreaming(
fetch("engine.wasm"),
{
env: {
gl_create_buffer() {
const buf = gl.createBuffer();
return registerGLObject(buf); // return integer handle
},
gl_buffer_data(handle, ptr, len, usage) {
const buf = getGLObject(handle);
const data = new Float32Array(memory.buffer, ptr, len / 4);
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, data, usage);
},
gl_draw_arrays(mode, first, count) {
gl.drawArrays(mode, first, count);
},
// ... more GL function wrappers
},
}
);
const memory = instance.exports.memory;
// Object handle registry for GL resources
const glObjects = [null]; // index 0 = null
function registerGLObject(obj) {
glObjects.push(obj);
return glObjects.length - 1;
}
function getGLObject(handle) {
return glObjects[handle];
}
instance.exports.init();
function frame() {
instance.exports.render_frame(performance.now());
requestAnimationFrame(frame);
}
frame();
Web Worker with Wasm
// main.js
const worker = new Worker("worker.js");
worker.onmessage = (e) => {
if (e.data.type === "result") {
console.log("Computed:", e.data.value);
}
};
worker.postMessage({
type: "compute",
input: new Float64Array([1, 2, 3, 4, 5]),
});
// worker.js
let instance;
async function init() {
const { instance: inst } = await WebAssembly.instantiateStreaming(
fetch("compute.wasm"),
{}
);
instance = inst;
}
const ready = init();
self.onmessage = async (e) => {
await ready;
if (e.data.type === "compute") {
const input = e.data.input;
const memory = instance.exports.memory;
const alloc = instance.exports.alloc;
const compute = instance.exports.compute;
// Copy input into Wasm memory
const ptr = alloc(input.byteLength);
new Float64Array(memory.buffer, ptr, input.length).set(input);
const result = compute(ptr, input.length);
self.postMessage({ type: "result", value: result });
}
};
Audio Processing with AudioWorklet
// main.js
const audioCtx = new AudioContext();
await audioCtx.audioWorklet.addModule("wasm-audio-processor.js");
const node = new AudioWorkletNode(audioCtx, "wasm-synth");
node.connect(audioCtx.destination);
// wasm-audio-processor.js
class WasmSynth extends AudioWorkletProcessor {
constructor() {
super();
this.ready = false;
this.init();
}
async init() {
const response = await fetch("synth.wasm");
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes, {});
this.wasm = instance.exports;
this.wasm.init(sampleRate);
this.ready = true;
}
process(inputs, outputs, parameters) {
if (!this.ready) return true;
const output = outputs[0][0]; // mono channel
const memory = this.wasm.memory;
const ptr = this.wasm.get_audio_buffer_ptr();
// Wasm fills its internal buffer with 128 samples
this.wasm.generate_samples(128);
// Copy from Wasm memory to output
const samples = new Float32Array(memory.buffer, ptr, 128);
output.set(samples);
return true;
}
}
registerProcessor("wasm-synth", WasmSynth);
Lazy Loading and Code Splitting
// Compile once, instantiate many times
let compiledModule = null;
async function getModule() {
if (!compiledModule) {
compiledModule = await WebAssembly.compileStreaming(fetch("app.wasm"));
}
return compiledModule;
}
// Instantiate on demand
async function createInstance(imports) {
const module = await getModule();
return await WebAssembly.instantiate(module, imports);
}
// Cache in IndexedDB for offline use
async function cacheModule() {
const module = await getModule();
const db = await openDB("wasm-cache", 1);
// Structured clone of WebAssembly.Module is supported
await db.put("modules", module, "app");
}
Content Security Policy
<!-- CSP header must allow wasm-unsafe-eval for Wasm compilation -->
<meta http-equiv="Content-Security-Policy"
content="script-src 'self' 'wasm-unsafe-eval'">
Best Practices
- Use
instantiateStreaming— it compiles while downloading and is the fastest way to load Wasm in the browser. Ensure the server sendsContent-Type: application/wasm. - Offload heavy computation to Web Workers — Wasm computation on the main thread blocks the UI. Use workers for any task taking more than 16ms.
- Cache compiled modules —
WebAssembly.Modulesupports structured cloning. Store compiled modules in IndexedDB to skip recompilation on subsequent page loads. - Use SIMD for data-parallel workloads — image filters, physics, matrix math, and signal processing all benefit enormously from Wasm SIMD.
- Keep Wasm focused on computation — let JavaScript handle DOM, layout, and browser API calls. Wasm excels at number crunching, not string manipulation or DOM traversal.
- Serve with proper MIME type —
application/wasmis required for streaming compilation. Misconfigured servers cause fallback to slower array-buffer compilation.
Common Pitfalls
- Blocking the main thread — a long-running Wasm function freezes the UI. There is no preemption; the browser cannot interrupt Wasm execution mid-function.
- Missing
wasm-unsafe-evalin CSP — strict Content Security Policies block Wasm compilation. Thewasm-unsafe-evaldirective is specifically for Wasm and does not weaken script protections. - Wrong MIME type — if the server sends
.wasmfiles asapplication/octet-stream,instantiateStreamingthrows aTypeError. Configure the server to useapplication/wasm. - Memory growth invalidating views — if Wasm grows memory during a render frame, any
Uint8ArrayorImageDataviews created before the growth become detached. Re-derive views frommemory.buffereach frame. - Oversized initial downloads — large Wasm binaries (>1MB) hurt time-to-interactive. Use
wasm-optto shrink, split into multiple modules, or lazy-load non-critical modules. - Assuming thread support —
SharedArrayBufferrequires cross-origin isolation headers (COOPandCOEP). Without these headers, shared memory and atomics are unavailable, and multi-threaded Wasm will not work.
Core Philosophy
Browser-side Wasm is a computational accelerator, not a replacement for JavaScript. The browser already has a high-performance JavaScript engine, a mature DOM API, and decades of web platform evolution. Wasm should be used where JavaScript is measurably too slow: image processing, physics simulations, audio synthesis, video codecs, and cryptographic operations. For DOM manipulation, event handling, and network requests, JavaScript is faster and more ergonomic.
Offload heavy computation to Web Workers. Wasm running on the main thread blocks the UI just like any other synchronous code — there is no preemption. A 100ms Wasm computation makes the page unresponsive and drops animation frames. Move intensive work to a Web Worker, post the results back to the main thread, and keep the UI smooth.
Manage the loading pipeline carefully. A Wasm binary must be downloaded, compiled, and instantiated before it can execute. For large modules (>1MB), this pipeline takes hundreds of milliseconds. Use instantiateStreaming to overlap download and compilation, cache compiled modules in IndexedDB for repeat visits, and lazy-load non-critical modules so they do not block the initial render.
Anti-Patterns
-
Running long computations on the main thread — a Wasm function that takes 200ms freezes the UI for 200ms with no way to interrupt it; use Web Workers for any computation exceeding 16ms.
-
Loading all Wasm modules on initial page load — downloading and compiling multiple large modules at startup delays time-to-interactive; lazy-load modules on demand when the user actually needs the feature.
-
Serving
.wasmfiles with the wrong MIME type —instantiateStreamingrequiresContent-Type: application/wasm; misconfigured servers cause fallback to slower ArrayBuffer-based compilation. -
Creating ImageData views before checking memory stability — if Wasm grows memory during a render frame, previously created
Uint8ClampedArrayviews become detached; re-derive views frommemory.buffereach frame. -
Ignoring CSP requirements for Wasm — strict Content Security Policies block Wasm compilation; the
wasm-unsafe-evaldirective is specifically designed for Wasm and does not weaken script protections.
Install this skill directly: skilldb add webassembly-skills
Related Skills
Assemblyscript
Writing WebAssembly modules using AssemblyScript, a TypeScript-like language that compiles to Wasm
Js Interop
JavaScript and WebAssembly interop patterns including memory sharing, type marshaling, and binding generation
Performance
Optimizing WebAssembly performance including binary size, execution speed, memory usage, and profiling techniques
Rust WASM
Compiling Rust to WebAssembly using wasm-pack, wasm-bindgen, and the Rust Wasm ecosystem
Wasi
WebAssembly System Interface (WASI) for portable system-level access including filesystem, networking, and clocks
WASM Basics
WebAssembly fundamentals including module structure, types, memory model, and binary/text formats