Web Workers
Date: 2026-08-17
A second JavaScript thread, with no access to the DOM and no shared variables — communication is by copied messages only. That constraint is what makes them safe and what makes them awkward, and it’s why they solve computation problems and not rendering ones.
A web worker is a script running in a separate background thread from the page, communicating with it only by passing messages.
What they actually give you
ASYNC (promises, await) WEB WORKER
still ONE thread a SECOND thread
a long function still blocks runs genuinely in parallel
"non-blocking" means it yields the main thread is free throughout
between operations, not that
it runs elsewhere
→ solves WAITING → solves COMPUTING
This is the distinction that decides whether a worker helps. await doesn’t make a 400ms loop stop blocking — it’s still on the main thread. Only a worker moves the work off it — The Event Loop, Long Tasks and Blocking.
The smallest working form
// main.js
const worker = new Worker('/worker.js');
worker.postMessage({ products, filters }); // structured clone: COPIED
worker.onmessage = (e) => {
render(e.data.results); // back on the main thread
};// worker.js — no window, no document, no DOM
self.onmessage = (e) => {
const { products, filters } = e.data;
const results = expensiveFilterAndSort(products, filters); // blocks
self.postMessage({ results }); // only HERE
};The worker has no DOM. It can’t read an element, can’t write one, can’t call getComputedStyle. It has fetch, IndexedDB, WebAssembly, timers and most of the language — everything except the document.
The cost: messages are copied
Data passed between threads is structured-cloned — serialised, copied, deserialised. For large payloads that copy happens on the main thread and can itself cause a stall.
50,000 product objects
postMessage(products) ~40–80ms of main-thread blocking to clone
← you may have spent more than you saved
Two escapes:
// TRANSFER: hand over ownership. near-instant, but the sender loses access
const buffer = new ArrayBuffer(1024 * 1024);
worker.postMessage(buffer, [buffer]);
// buffer.byteLength is now 0 on this side — it's gone
// SHARE: SharedArrayBuffer, genuinely shared memory
// requires cross-origin isolation headers, and reintroduces
// every classic concurrency hazard — Race ConditionsThe rule of thumb: a worker pays off when the computation is long and the data crossing the boundary is small. Parsing a 4 MB CSV in a worker and returning 200 summary rows is an excellent trade. Passing 50,000 objects back and forth to save 30ms of work is a loss.
Where they genuinely help in commerce
- Client-side search and filtering over a large catalogue held in memory — the case where a worker most clearly wins
- Parsing large responses.
JSON.parseon a multi-megabyte payload blocks; doing it in a worker doesn’t - Image manipulation before upload — resizing, EXIF stripping — via
OffscreenCanvas, which is one of the few rendering-adjacent APIs workers can use - Analytics batching and processing, keeping event serialisation off the critical thread — Event Batching and Delivery
- Cryptographic work, hashing, and anything computational in a checkout flow
Where they don’t help
- DOM manipulation. The most-wanted use, and permanently unavailable. A slow render is not a worker problem — Reflow and Repaint
- Network waiting.
fetchis already non-blocking on the main thread; a worker adds nothing - Short tasks. Worker startup is a few milliseconds plus the script parse, so anything under ~50ms is usually not worth moving
- Anything needing synchronous access to main-thread state
The relatives
DEDICATED WORKER one page owns it. the default.
SHARED WORKER several tabs of the same origin share one instance
SERVICE WORKER a network proxy, not a computation thread —
different purpose entirely
— Cache API and Service Workers
WORKLETS tiny, specialised (audio, paint, layout)
The third row is a different tool entirely — Cache API and Service Workers.
Service workers get confused with web workers constantly. Both are off-main-thread scripts; a service worker exists to intercept network requests and survive page navigations, and using one for computation is the wrong tool.
Practical
- Use a wrapper library unless the messaging is trivial. Hand-rolled
postMessageprotocols become an ad-hoc RPC layer with no types and no error handling - Handle errors explicitly. An uncaught exception in a worker fires
onerroron the worker object and does not surface as a normal error — it will be invisible in your error tracking unless wired up — Error Tracking - Terminate workers you’re done with —
worker.terminate(). A worker per component that’s never cleaned up is a memory leak with a thread attached — Memory and Long Sessions - Measure before and after. The clone cost is easy to underestimate and the win is easy to assume — Interaction to Next Paint
- Feature-detect and degrade. The synchronous version should still work, just slower — Progressive Enhancement
Where it interacts
- The Event Loop — the single-thread model workers are the exception to
- Long Tasks and Blocking — the problem workers solve, and the alternative fixes that are usually cheaper
- Tasks and Microtasks — yielding on the main thread, which handles many cases without a worker at all
- Race Conditions — the hazards message-passing avoids, and
SharedArrayBufferreintroduces