Topics
v6.0.0
On this page · sections

Web Workers

Web Workers run JavaScript on a separate thread with no DOM access — the standard browser tool for CPU-heavy work without freezing the main thread.

ConceptWhat it isWhen to use
Dedicated WorkerOne-to-one thread per pageParse, compress, sort, crypto
postMessageStructured clone between threadsDefault data transfer
TransferablesZero-copy ArrayBuffer handoffLarge binary payloads
Service WorkerNetwork proxy / offlineDifferent lifecycle — not general compute
SharedArrayBufferShared memoryNeeds COOP/COEP; rare in frontend

What Workers Are

Definition

A Web Worker has its own global scope — no window, no DOM, no document. Spawn with new Worker(url) and communicate via postMessage.

// main.js const worker = new Worker('heavy.js'); worker.onmessage = (event) => { console.log('Result:', event.data.sum); }; worker.postMessage({ count: 100000 }); // heavy.js self.onmessage = (event) => { const { count } = event.data; let sum = 0; for (let i = 0; i < count; i++) sum += i; self.postMessage({ sum }); };

Use workers when CPU-heavy work — parsing megabytes of JSON, image resize, sorting large arrays — would block the main thread and kill INP.

Workers share nothing by default. They fix CPU saturation, not I/O wait — fetch is already async on main.

Interview: Main thread owns DOM and input; workers own compute. Startup + clone cost means tiny tasks may be slower on workers.

postMessage and Transferables

Definition

postMessage uses the structured clone algorithm — deep-copies most objects. Cloning large arrays is expensive.

Transferables (typically ArrayBuffer) move ownership zero-copy — the sender’s buffer becomes detached.

const worker = new Worker('image-worker.js'); function processImage(pixelBuffer) { worker.postMessage( { type: 'PROCESS', width: 800, height: 600 }, [pixelBuffer] ); // pixelBuffer.byteLength === 0 — detached } worker.onmessage = (event) => { const { type, outputBuffer } = event.data; if (type === 'DONE') { console.log('Received bytes:', outputBuffer.byteLength); } };

Design messages as typed envelopes — { type: 'PARSE_DONE', payload }. Cannot clone: functions, DOM nodes, symbols.

MessageChannel provides bidirectional ports. Serialization runs synchronously on send — huge objects block the calling thread.

Interview: Transfer is zero-copy handoff; sender loses the buffer. Batch small messages to amortize overhead.

Production Use Cases

Definition

Image resize before upload: main reads file → transfer ArrayBuffer → worker uses OffscreenCanvas → encode WebP → postMessage blob back → main uploads.

CSV import: stream file chunks to worker, parse lines, send { type: 'PROGRESS', pct } every N rows — main updates progress bar only.

const worker = new Worker('csv-worker.js'); let cancelled = false; worker.onmessage = (event) => { const { type, rows, total, error } = event.data; if (type === 'PROGRESS') updateProgressBar(rows / total); if (type === 'DONE') renderTable(event.data.summary); if (type === 'ERROR') showError(error); }; async function pumpChunks(reader) { while (!cancelled) { const { done, value } = await reader.read(); if (done) { worker.postMessage({ type: 'EOF' }); break; } worker.postMessage({ type: 'CHUNK', data: value }, [value.buffer]); } }

Do not return 500k row arrays to main — aggregate in worker; send summary + paginate. WASM in workers is common for compression and codecs.

Interview: Keep UI state on main; worker returns immutable results. OffscreenCanvas draws without touching main thread.

Limitations and Lifecycle

Definition

Workers cannot touch the DOM, window, or most browser chrome APIs. They don’t share lexical scope with main.

let activeWorker = null; function startJob(payload) { stopJob(); activeWorker = new Worker('job.js'); activeWorker.onmessage = (e) => handleResult(e.data); activeWorker.postMessage(payload); } function stopJob() { if (activeWorker) { activeWorker.terminate(); // immediate kill — no cleanup callback activeWorker = null; } } // React: useEffect(() => () => stopJob(), []);

Leaks happen when workers stay alive after navigation — always terminate() on SPA unmount. self.close() exits gracefully from inside. Worker errors don’t bubble to window — use worker.onerror.

Spawn-per-keystroke is expensive — pool or debounce. React Strict Mode can double-mount workers in dev — guard with ref or singleton.

Interview: removeEventListener needs same reference; same rule applies — terminate workers in effect cleanup.

SharedArrayBuffer and Worker Pools

Definition

SharedArrayBuffer gives true shared memory — no clone on every tick. Browsers gate SAB behind cross-origin isolation:

  • Cross-Origin-Opener-Policy: same-origin
  • Cross-Origin-Embedder-Policy: require-corp

Use Atomics for synchronization. COEP can break third-party embeds without Cross-Origin-Resource-Policy.

For most apps, transferable postMessage is simpler. At scale, run a worker pool sized to hardwareConcurrency - 1:

class WorkerPool { constructor(size, scriptUrl) { this.queue = []; this.idle = []; for (let i = 0; i < size; i++) { const w = new Worker(scriptUrl); w.onmessage = (e) => this.#onDone(w, e.data); this.idle.push(w); } } run(payload) { return new Promise((resolve) => { const job = { payload, resolve }; const w = this.idle.pop(); w ? this.#assign(w, job) : this.queue.push(job); }); } #assign(worker, job) { worker._job = job; worker.postMessage(job.payload); } #onDone(worker, result) { worker._job.resolve(result); const next = this.queue.shift(); next ? this.#assign(worker, next) : this.idle.push(worker); } }

Interview: postMessage + transfer covers 95%; SAB only when profiling proves clone bottleneck and COOP/COEP cost is acceptable.

Senior Interview Summary

Mental model

Main thread → DOM, input, paint scheduling Worker → CPU-bound JS, no DOM postMessage → Structured clone (default) or transfer (zero-copy)

Practical rules

  • Workers for CPU, not network latency
  • Typed message protocol with { type, payload }
  • Terminate on SPA unmount; cap worker cache size
  • Pool workers for repeated jobs; backpressure on queue
  • Benchmark threshold — ~50ms+ tasks often worth it

What to say in interviews

“I’d offload parse/compress to a worker with transferables for binary data, use a typed protocol with progress and cancel messages, and terminate on route change. For high-frequency ticks I’d pool workers sized to hardwareConcurrency minus one. SAB only if structured clone is the measured bottleneck.”

Common traps

  • “Workers share memory” — default is structured clone; SAB needs COOP/COEP
  • “Transfer copies and keeps original” — sender buffer is detached
  • “Workers can update the DOM” — post results back; main updates UI
  • “Enable SAB everywhere” — COEP breaks non-compliant third-party widgets

Last updated on