Without Worker — Main Thread❌ Freezes UI
// ❌ 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 (
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();
}
};