← 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