Web Workers Overview
Move CPU work off the main thread — dedicated workers, messaging, transferables, limits, and when workers aren’t worth it.
- javascript
- web-workers
Long JavaScript on the main thread freezes input and janks frames. Web Workers run scripts in parallel threads with no DOM access. You talk via message passing (structured clone / transfer).
// main.js
const worker = new Worker(new URL('./heavy.js', import.meta.url), {
type: 'module',
});
worker.postMessage({ type: 'sum', nums: bigArray });
worker.onmessage = (e) => {
console.log('result', e.data);
};
worker.onerror = (e) => console.error(e.message);
// later
worker.terminate();
// heavy.js
self.onmessage = (e) => {
const { type, nums } = e.data;
if (type === 'sum') {
const total = nums.reduce((a, b) => a + b, 0);
self.postMessage({ type: 'sum', total });
}
};
What workers can / can’t do
| Can | Cannot |
|---|---|
| CPU-heavy JS, crypto, parse | Touch window / DOM |
fetch, timers, IndexedDB (dedicated) |
Assume shared memory without SAB |
| Import scripts / modules | Synchronously block main for free |
OffscreenCanvas exists for some graphics paths — special case, not free DOM.
Message cost
// structured clone of huge arrays costs CPU + memory
worker.postMessage(hugeObject);
// transfer ArrayBuffer ownership — zero-copy
worker.postMessage(buf, [buf]);
// buf detached in sender
Design protocols with small messages or transferables. Don’t post the entire app state every frame.
Module workers vs classic
new Worker('classic.js'); // importScripts inside
new Worker(url, { type: 'module' }); // import/export, better bundler fit
Vite/webpack provide new URL(..., import.meta.url) patterns so hashes resolve.
SharedWorker / Service Worker (names only)
- SharedWorker — shared among tabs of an origin (limited support/use).
- Service Worker — network proxy / offline; different lifecycle.
Don’t confuse with dedicated workers for compute.
When not to use a worker
- Work is tiny (message overhead dominates)
- You need immediate DOM measurement
- You’re only waiting on network (async on main is enough)
Workers shine for image processing, parsing large JSON/CSV, encryption, physics, syntax highlighting.
Error and lifecycle hygiene
useEffect(() => {
const w = new Worker(new URL('./w.js', import.meta.url), { type: 'module' });
w.onmessage = handle;
return () => w.terminate();
}, []);
Leaking workers keeps threads and memory alive across SPA navigations.
Interview answer (out loud)
“Dedicated workers run JS off the main thread without DOM. We communicate with postMessage using structured clone or transferable buffers. Good for CPU-bound work; bad for tiny tasks or DOM. I terminate on unmount and prefer module workers with bundler URL resolution.”
Pooling
Creating a worker per task has startup cost. For lots of similar jobs, keep a small pool and a task queue:
// sketch: worker free → post next job; onmessage → resolve promise map by id
Libraries (comlink) reduce boilerplate for RPC-style APIs over postMessage.
Cross-origin workers
Worker scripts are same-origin by default with classic paths; module workers and CDN scripts need correct CORS. Prefer same-origin bundled workers for simplicity.
Debugging
Chrome DevTools lists workers under the page; breakpoints work inside worker files. Log with a [worker] prefix — interleaved main/worker logs confuse otherwise.
Further reading
Related
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.