Without Worker — Main Thread❌ Freezes UI
⚠ UI FROZEN
// ❌ Blocks main thread for ~2s function heavyTask() { let result = 0; for (let i = 0; i < 800_000_000; i++) { result += Math.sqrt(i); } return result; } // Ball stops, clicks ignored, page frozen
With Worker — Background Thread✅ UI stays smooth
// ✅ Inline worker — no extra file const code = `self.onmessage = e => { let r = 0; for (let i=0; i<800_000_000; i++) r += Math.sqrt(i); self.postMessage(r); };`; const w = new Worker( URL.createObjectURL( new Blob([code]) ) );
Time (UI froze)
Time (UI smooth)
Worker Pool — 4 Workers, 8 TasksParallel processing
Worker 1
Idle
Worker 2
Idle
Worker 3
Idle
Worker 4
Idle
0
Tasks complete
Total time (ms)
Pool Implementation
// Worker pool with task queue class WorkerPool { constructor(script, size) { this.workers = []; this.idle = []; this.queue = []; this.tasks = new Map(); for (let i = 0; i < size; i++) { const w = new Worker(script); w.onmessage = e => this._done(i, e); this.workers.push(w); this.idle.push(i); } } run(data) { return new Promise((res, rej) => { const id = this.taskId++; this.tasks.set(id, {res, rej}); if (this.idle.length > 0) this._dispatch(this.idle.pop(), id, data); else this.queue.push({id, data}); }); } }
Transferable Objects BenchmarkCopy vs Transfer
Copy 64MB ArrayBuffer
Transfer 64MB ArrayBuffer
Speed difference
Copy — data is serialised and cloned. Original reference still valid. Expensive for large buffers.
Transfer — ownership moves to the worker. Original becomes empty (byteLength === 0). Zero-copy, near instant.
// Transfer: postMessage(data, transferables[]) const buf = new ArrayBuffer(64 * 1024 * 1024); // Copy (default) — slow for large data worker.postMessage({ buf }); // Transfer (zero-copy) — fast worker.postMessage({ buf }, [buf]); // buf.byteLength === 0 now — ownership moved
Benchmark Details
// Benchmark: measure copy vs transfer time function benchmarkCopy() { const buf = new ArrayBuffer(64 * 1024 * 1024); const t0 = performance.now(); // No transferables list = copy worker.postMessage({ buf }); console.log('Copy:', performance.now() - t0, 'ms'); // buf is still valid — not transferred } function benchmarkTransfer() { const buf = new ArrayBuffer(64 * 1024 * 1024); const t0 = performance.now(); // Pass [buf] as transferables = zero-copy worker.postMessage({ buf }, [buf]); console.log('Transfer:', performance.now() - t0, 'ms'); // buf.byteLength === 0 — ownership moved }
Worker Sending Progress UpdatesLive progress bar
Ready 0%
// worker.js — send progress updates self.onmessage = function({ data }) { const total = data.items.length; for (let i = 0; i < total; i++) { heavyProcess(data.items[i]); // Report every 5% if (i % (total / 20) === 0) { self.postMessage({ type: 'progress', pct: Math.round(i / total * 100) }); } } self.postMessage({ type: 'done' }); };
Main Thread Handler
0
Items processed
0
Elapsed (ms)
The bouncing ball below proves the UI stays responsive while the worker sends progress updates. The ball never pauses — only the worker thread is busy.
// main.js — receive progress worker.onmessage = ({ data }) => { if (data.type === 'progress') { progressBar.style.width = data.pct + '%'; label.textContent = data.pct + '% done'; } if (data.type === 'done') { worker.terminate(); } };
Read the tutorial