Skip to main content
Technology & EngineeringWebassembly324 lines

WASM in Browser

Using WebAssembly in the browser for canvas rendering, audio processing, Web Workers, and DOM integration

Quick Summary34 lines
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 lines
Paste into your CLAUDE.md or agent config

Browser-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

  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

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 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 modulesWebAssembly.Module supports 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 typeapplication/wasm is 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-eval in CSP — strict Content Security Policies block Wasm compilation. The wasm-unsafe-eval directive is specifically for Wasm and does not weaken script protections.
  • Wrong MIME type — if the server sends .wasm files as application/octet-stream, instantiateStreaming throws a TypeError. Configure the server to use application/wasm.
  • Memory growth invalidating views — if Wasm grows memory during a render frame, any Uint8Array or ImageData views created before the growth become detached. Re-derive views from memory.buffer each frame.
  • Oversized initial downloads — large Wasm binaries (>1MB) hurt time-to-interactive. Use wasm-opt to shrink, split into multiple modules, or lazy-load non-critical modules.
  • Assuming thread supportSharedArrayBuffer requires cross-origin isolation headers (COOP and COEP). 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 .wasm files with the wrong MIME typeinstantiateStreaming requires Content-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 Uint8ClampedArray views become detached; re-derive views from memory.buffer each frame.

  • Ignoring CSP requirements for Wasm — strict Content Security Policies block Wasm compilation; the wasm-unsafe-eval directive is specifically designed for Wasm and does not weaken script protections.

Install this skill directly: skilldb add webassembly-skills

Get CLI access →