← Back to Lessons Lesson 19 of 28
Advanced concurrency

Wasm + Web Workers

Why Web Workers?

Wasm runs on the main thread by default. Heavy computation blocks the UI — buttons freeze, animations stop. Web Workers let you run Wasm on a separate thread.

Main Thread (UI)              Worker Thread (Computation)
┌──────────────┐              ┌──────────────┐
│ DOM, events  │  postMessage │ Wasm module  │
│ animations   │ ◀──────────▶│ heavy math   │
│ responsive   │              │ doesn't block│
└──────────────┘              └──────────────┘

Basic Worker Setup

worker.js

import init, { heavy_computation } from './pkg/my_wasm.js';

self.onmessage = async (e) => {
    await init();

    const { type, data } = e.data;

    if (type === 'compute') {
        const result = heavy_computation(data.iterations);
        self.postMessage({ type: 'result', result });
    }
};

main.js

const worker = new Worker('./worker.js', { type: 'module' });

worker.onmessage = (e) => {
    if (e.data.type === 'result') {
        console.log('Result:', e.data.result);
    }
};

// This doesn't block the UI
worker.postMessage({
    type: 'compute',
    data: { iterations: 10_000_000 }
});

Multiple Workers (Parallel)

Split work across multiple Workers for true parallelism:

function createWorkerPool(size) {
    const workers = [];
    for (let i = 0; i < size; i++) {
        workers.push(new Worker('./worker.js', { type: 'module' }));
    }
    return workers;
}

async function parallelPrimeCount(max, numWorkers = 4) {
    const pool = createWorkerPool(numWorkers);
    const chunkSize = Math.ceil(max / numWorkers);

    const promises = pool.map((worker, i) => {
        const start = i * chunkSize + 1;
        const end = Math.min((i + 1) * chunkSize, max);

        return new Promise((resolve) => {
            worker.onmessage = (e) => resolve(e.data.count);
            worker.postMessage({ type: 'count_primes', start, end });
        });
    });

    const counts = await Promise.all(promises);
    return counts.reduce((a, b) => a + b, 0);
}

SharedArrayBuffer (Advanced)

Share memory between Workers without copying:

// Requires these headers on your server:
// Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Embedder-Policy: require-corp

const shared = new SharedArrayBuffer(1024);
const view = new Float64Array(shared);

// Both main thread and worker can read/write
worker.postMessage({ buffer: shared });
// In Rust, access shared memory via pointer
#[wasm_bindgen]
pub fn process_shared(ptr: *mut f64, len: usize) {
    let data = unsafe { std::slice::from_raw_parts_mut(ptr, len) };
    for x in data.iter_mut() {
        *x *= 2.0;
    }
}

When to Use Web Workers

Use Case Main Thread Web Worker
DOM manipulation ✗ (no DOM access)
Event handlers
Heavy computation ✗ (blocks UI)
Image processing
Crypto operations
Physics simulation
Data parsing Depends on size ✓ for large files

Performance Comparison

Task Main Thread 1 Worker 4 Workers
Count primes to 1M 800ms (UI frozen) 800ms (UI responsive) 200ms
Image blur (4K) 120ms (UI frozen) 120ms (UI responsive) 35ms

Workers don't make individual tasks faster — they keep the UI responsive and enable parallelism.

Try It

The starter code shows CPU-intensive functions (trigonometric computation, prime counting) that would block the UI if run on the main thread. In production, wrap these in a Web Worker.

Try It

Chapter Quiz

Pass all questions to complete this lesson