What are Web Workers and when should you use them?
Advertisement
🧩 Scenario
Architecture Walkthrough
The Main Thread and Why Blocking It Matters
JavaScript in the browser runs on the main thread. The main thread is also responsible for layout, painting, user input handling, and animation frame callbacks. Any synchronous JavaScript that takes too long occupies the main thread for that entire duration. During this time, the browser cannot process clicks, cannot update the scroll position, and cannot repaint. This is what "jank" and "unresponsive UI" look like from the user's perspective.
The threshold for perceivable lag is approximately 50ms. Any synchronous operation that takes longer than this on the main thread will cause users to notice a delay. For compute-heavy tasks like parsing large files, encoding images, or running a simulation, offloading to a Worker removes the blocking from the main thread entirely.
Workers and Message Passing
A Web Worker runs in a separate OS thread with its own JavaScript runtime. It has no access to window, document, or any DOM API. It has access to fetch, XMLHttpRequest, crypto, IndexedDB, WebSockets, Canvas (OffscreenCanvas), and most other non-DOM Web APIs.
Communication between the main thread and a worker uses postMessage and onmessage. When you pass data via postMessage, the data is serialized using the structured clone algorithm and deserialized on the receiving end. This means the worker receives an independent copy of the data, not a reference to the original object. Mutations inside the worker do not affect the main thread's data.
Transferable Objects and SharedArrayBuffer
Serializing large data (a 100MB ArrayBuffer representing image pixels) via structured clone is expensive because it copies the entire buffer. Transferable objects avoid this by transferring ownership of the memory to the worker instead of copying it. After a transfer, the original object on the main thread becomes neutered (empty) and the worker owns the buffer. The transfer is O(1) regardless of buffer size.
SharedArrayBuffer enables true shared memory between threads. Both the main thread and the worker can read and write to the same memory simultaneously. This requires Atomics for synchronization to avoid data races, and it requires specific HTTP headers (Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp) for the browser to enable it.
Key Code Explained
// main.js
const worker = new Worker('worker.js');
// Send data to the worker
worker.postMessage({ numbers: new Array(1_000_000).fill(0).map(() => Math.random()) });
// Receive result without blocking the UI
worker.onmessage = (e) => {
console.log('Min:', e.data.min, 'Max:', e.data.max, 'Sum:', e.data.sum);
};
worker.onerror = (err) => {
console.error('Worker error:', err.message);
worker.terminate(); // clean up the OS thread
};
// Terminate after the job is done to free the thread
worker.onmessage = (e) => {
processResult(e.data);
worker.terminate();
};
// worker.js
self.onmessage = function (e) {
const { numbers } = e.data;
const min = Math.min(...numbers);
const max = Math.max(...numbers);
const sum = numbers.reduce((a, b) => a + b, 0);
self.postMessage({ min, max, sum });
};
// Transferable: send ArrayBuffer without copying (O(1) transfer)
const buffer = new ArrayBuffer(100 * 1024 * 1024); // 100MB
worker.postMessage({ buffer }, [buffer]); // buffer is transferred, not copied
// After this line: buffer.byteLength === 0 — main thread no longer owns it
// SharedArrayBuffer: shared memory (requires COOP/COEP headers)
const shared = new SharedArrayBuffer(4);
const view = new Int32Array(shared);
worker.postMessage({ shared });
// Both main thread and worker can read/write view[0] simultaneously
// Use Atomics.load, Atomics.store, Atomics.add for safe concurrent access
The worker.terminate() call in the onmessage handler is important. Workers are OS threads. Leaving a finished worker alive wastes system resources. For one-off tasks, terminate immediately after receiving the result. For recurring tasks (a worker that handles multiple jobs), reuse the same worker instance rather than spawning a new one per job.
Tradeoffs
| Approach | Pro | Con |
|---|---|---|
| Web Worker with postMessage | No main-thread blocking, easy data isolation | Serialization overhead, no DOM access, added complexity |
| Worker with Transferable objects | O(1) for large ArrayBuffers | Original buffer neutered after transfer |
| SharedArrayBuffer + Atomics | True zero-copy shared memory | Requires COOP/COEP headers, race condition risk without Atomics |
| Main thread only | Simple, no IPC overhead | Blocks UI for long computations |
What Interviewers Actually Check
- Whether you can explain why blocking the main thread causes UI freezes
- Whether you know that workers have no DOM access
- Whether you know that
postMessagecopies data using structured clone, not by reference - Whether you know when the overhead of workers is not worth it for fast tasks
- Whether you can explain Transferable objects and why they exist
Follow-Up Questions
- What is an inline worker and how do you create one from a Blob URL instead of a separate file?
- What is a Service Worker and how does it differ from a Web Worker in terms of scope and lifecycle?
- How would you build a worker pool to reuse N worker threads for M tasks?
- What types can be transferred as Transferable objects in a
postMessagecall? - If you have 8 CPU cores available, how many workers would you spin up for a parallelizable computation and why?
Common Candidate Mistakes
- Trying to access
documentorwindowfrom inside a worker and not knowing these are unavailable in the worker global scope - Assuming
postMessagepasses data by reference when it copies it using structured clone - Using a worker for a task that runs in 2ms when the
postMessageround-trip overhead is comparable to the task itself - Not calling
worker.terminate()after a one-off task, leaving idle OS threads consuming resources - Not knowing that
SharedArrayBufferrequires specific HTTP headers to be enabled in modern browsers due to Spectre mitigations
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you explain what the main thread is and why blocking it freezes the UI?
- Can you write a basic worker that receives data via
onmessageand posts a result back? - Can you explain the structured clone algorithm and what types it can and cannot transfer?
- Can you describe when a Web Worker is worth the added complexity vs overkill?
- Can you explain the difference between message-passing and
SharedArrayBufferfor inter-thread communication?
Summary
Web Workers run JavaScript in a separate OS thread, freeing the main thread to handle user interaction, layout, and painting without interruption. Workers communicate with the main thread exclusively via postMessage and onmessage. Data passed through postMessage is copied using the structured clone algorithm, so the worker operates on an independent copy and cannot directly mutate main-thread data.
For large binary data like ArrayBuffer, Transferable objects avoid the copy cost by transferring ownership in O(1) time. The main thread loses access to the buffer after transfer. SharedArrayBuffer goes further and enables true shared memory between threads, but it requires careful synchronization with Atomics and specific HTTP security headers.
Workers are worthwhile when a computation takes longer than roughly 50ms and needs to run without blocking the browser's rendering pipeline. Image processing, CSV parsing, cryptographic operations, and physics simulations are strong candidates. For tasks under 10ms, the postMessage round-trip and worker startup overhead may exceed the task duration itself, making a worker counterproductive.
Can Web Workers access the DOM?
No. They run in a separate thread without access to window, document, or any DOM APIs.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement