JavaScript Web Workers Table

PieceWhat it doesField note
new Worker('w.js')Spawn a real threadOwn event loop, own globals - heavy work stops freezing the page
postMessage(data)Send a messageSTRUCTURED CLONE by default - sent copies, not shared references
onmessageReceive in worker/mainBoth sides have postMessage/onmessage - the only shared door
Transferable objectsZero-copy handoffArrayBuffer moves by transfer - sender loses it (fast, one-way)
self-importScripts / ESMLoad code in worker{ type: 'module' } unlocks imports inside the worker
NO DOM in workersdocument is undefinedNo window, no elements - pure computation plus fetch/IndexedDB
worker.terminate()Kill from outsideHard stop - no cleanup inside; design messages to finish first
OffscreenCanvasEven graphics movePaint in a worker - the main thread keeps 60fps while pixels render
Reference: the MDN Web Workers reference. A worker is a REAL thread with its own event loop - the fix for every long task that freezes the UI. The contract has two clauses: communication is message-passing with STRUCTURED CLONE (data copies; a mutation after send is invisible to the other side), and workers see NO DOM - no document, no elements, just computation, fetch and IndexedDB. Bottom line: move parsing, image processing and big loops off the main thread, transfer ArrayBuffers when the payload is huge (zero-copy), and remember terminate() is a hard stop with no inside cleanup. Related tools: event loop table (the main thread the worker unblocks), async/await table (concurrency on one thread vs real threads), and canvas table (OffscreenCanvas moves painting too).

A Web Worker is a REAL thread with its own event loop and globals - the fix for every long task that freezes the page: JSON parsing of huge payloads, image processing, search over big datasets, anything over about 50ms that steals frames from the main thread.

Bottom line: communication is message-passing with STRUCTURED CLONE - postMessage copies the data, so a mutation after send is invisible to the other side (not a shared reference, a delivered package). And workers see NO DOM: no document, no elements - pure computation plus fetch and IndexedDB.

The honest part: workers are not free. Spawning costs tens of milliseconds, messages copy (unless you transfer), and the message-passing architecture turns simple code into protocol code. The threshold that pays: work that blocks the main thread long enough to drop frames - not a loop that takes 5ms.

How to use

  1. Offload the heavy step: const w = new Worker('parse.js'); w.postMessage(bigJson) - the page keeps 60fps while the worker chews.
  2. Hand off buffers zero-copy: w.postMessage(buffer, [buffer]) - the ArrayBuffer TRANSFERS (sender loses it), skipping the clone entirely for big payloads.
  3. Keep the protocol tidy: one message type object ({type, payload}) both directions, and finish work before terminate() - it is a hard stop with no cleanup.

Frequently asked questions

What does structured clone actually copy - and what does it refuse?

It deep-copies plain data: objects, arrays, Map, Set, Date, RegExp, ArrayBuffer and typed arrays - more than JSON survives (undefined, NaN, Map and Set all make it), and circular references work, which JSON.stringify cannot do. It refuses functions, DOM nodes, and Error prototypes degrade - the things that cannot cross a thread boundary meaningfully. The practical rule: postMessage delivers a deep copy with richer type support than JSON, but it is still a COPY - two clones of one object do not stay synchronized, which is why SharedArrayBuffer exists for the rare true-sharing case.

Why would I transfer an ArrayBuffer instead of just posting it?

Because cloning a 100MB buffer means copying 100MB; transferring moves OWNERSHIP in O(1) - the second postMessage argument lists transferables, and the buffer's bytes never copy, but the sender's reference is NEUTERED (byteLength 0). It is a move, not a share. The pattern: read a file into a buffer on the main thread, transfer it to the worker for processing, transfer the result back - two zero-copy hops around the expensive computation. The footgun: using the buffer after transferring throws or reads nothing, so treat transferred references as dead immediately.

What can a worker NOT do, and how do I design around it?

Workers have no DOM: no document, no elements, no window - so they cannot touch the page, read layout, or draw directly. The design is split-brain by intention: the worker computes (parse, transform, search, hash), the main thread renders the result. Also absent: localStorage (synchronous, main-thread-tied - use IndexedDB, which IS available in workers), and some APIs like alert. Modern additions keep shrinking the gap: fetch works, OffscreenCanvas moves painting into the worker, and module workers ({ type: 'module' }) allow real imports - but the DOM line stands.

When is a worker the wrong tool for a slow operation?

When the cost is I/O latency, not CPU, or the workload is shorter than the worker's spawn cost. A slow fetch is already off-thread - async/await on the main thread handles it, and a worker adds protocol overhead for nothing. A computation that takes 5-20ms does not justify tens of milliseconds of worker startup plus clone time; debounce it or chunk it instead. The honest threshold: repeated or occasional work that measurably drops frames (long tasks over ~50ms in the performance panel) - that is what workers exist to eliminate, and the panel before-and-after is the proof.

Related tools