What are Web Workers and when should you use them?

Advanced16 min interview
Skills tested:
How Web Workers run in a separate OS thread with no DOM accesspostMessage and onmessage for bidirectional communicationStructured clone algorithm for data transfer between threadsSharedArrayBuffer and Atomics for true shared memoryWhen Web Workers provide a real benefit vs when they add unnecessary overhead

Advertisement

🧩 Scenario

In a real codebase, Web Workers are used when a computation is CPU-intensive enough to freeze the main thread for more than about 50ms. Image processing, video encoding, large dataset sorting, cryptography, and complex physics simulations are all good candidates. The overhead of spawning a worker and serializing data via postMessage is real, so workers are not worth the complexity for fast tasks.

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

ApproachProCon
Web Worker with postMessageNo main-thread blocking, easy data isolationSerialization overhead, no DOM access, added complexity
Worker with Transferable objectsO(1) for large ArrayBuffersOriginal buffer neutered after transfer
SharedArrayBuffer + AtomicsTrue zero-copy shared memoryRequires COOP/COEP headers, race condition risk without Atomics
Main thread onlySimple, no IPC overheadBlocks 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 postMessage copies 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

  1. What is an inline worker and how do you create one from a Blob URL instead of a separate file?
  2. What is a Service Worker and how does it differ from a Web Worker in terms of scope and lifecycle?
  3. How would you build a worker pool to reuse N worker threads for M tasks?
  4. What types can be transferred as Transferable objects in a postMessage call?
  5. 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 document or window from inside a worker and not knowing these are unavailable in the worker global scope
  • Assuming postMessage passes data by reference when it copies it using structured clone
  • Using a worker for a task that runs in 2ms when the postMessage round-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 SharedArrayBuffer requires 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 onmessage and 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 SharedArrayBuffer for 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.

Related Videos
Watch these videos to learn more about this topic
Frequently Asked Questions

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