The main thread does everything: parse HTML, run JavaScript, handle input, calculate layout, paint. Every heavy computation you run there delays all of those other tasks. Web Workers move computation to a background thread, keeping the main thread free for the things only it can do.
What Web Workers Are
A Web Worker is a JavaScript file running in a background thread. It has no access to the DOM, window, or document — it operates in isolation. Communication with the main thread happens through message passing.
postMessage serializes data via the structured clone algorithm. For large payloads, this serialization cost can be significant.
Transferable Objects
To avoid serialization overhead for large binary data (ArrayBuffers, ImageBitmaps), use Transferable Objects. Ownership is transferred to the worker — the original reference becomes unusable, but no copying occurs.
This is essential for image processing, audio worklets, and data pipelines passing large arrays between threads.
What to Move to a Worker
Good candidates:
- JSON parsing of large API responses (
JSON.parseon 5MB+ payloads) - Cryptographic operations (hashing, encryption)
- Image processing and filtering
- CSV/spreadsheet parsing
- Physics simulations and complex calculations
- Compression/decompression
Bad candidates:
- Anything that needs the DOM
- Short, fast operations where message-passing overhead exceeds the computation time
- Code that needs synchronous access to shared state
Here's a concrete example of offloading JSON parsing — a common bottleneck on data-heavy dashboards:
Shared Workers and Service Workers
Shared Workers are shared across multiple tabs of the same origin. Useful for shared state like a WebSocket connection that all tabs read from.
Service Workers are a different concept — they intercept network requests and manage caching. They don't run heavy computation; they proxy the network.
OffscreenCanvas
OffscreenCanvas moves canvas rendering to a Web Worker. This is the tool for heavy canvas operations (games, charts, video processing) that would otherwise block the main thread on every frame.
Once transferControlToOffscreen is called, the main thread can no longer draw to that canvas. The worker owns it entirely — which means a 60fps animation loop in the worker never competes with user input handling on the main thread.
Module Workers
Modern browsers support ES module syntax in workers via { type: 'module' }:
This enables import statements inside the worker, making it easier to share utility code between the main thread and workers.
Worker Pools
For tasks that recur frequently (processing a stream of items), creating a new Worker per task is expensive. A worker pool maintains a fixed set of workers and distributes work across them. Size the pool based on the available CPU cores:
Libraries like comlink and workerpool handle this pattern, including promise-based message passing that removes the manual onmessage boilerplate.
The manual postMessage / onmessage dance is gone. Comlink handles the message routing and wraps everything in promises.
Workers are one of those tools that feel overly complex until you actually need them — then you can't imagine shipping without them.
