Volume 4

Q222: Web Workers, Off Main Thread Work and CPU Heavy Tasks

Difficulty: SeniorFrequency: MediumAnswer time: 8-12 minutes

What Interviewers Want To Evaluate

Interviewers want to know whether you understand when moving work off the main thread helps and when it only adds complexity.

They check Web Workers, message passing, structured cloning, transferable objects, worker lifecycle, bundling, TypeScript contracts, UI responsiveness, and fallback strategy.

Short Interview Answer

Web Workers let JavaScript run on a background thread so CPU-heavy work does not block input and rendering on the main thread. I use workers for expensive parsing, search, compression, syntax analysis, data transforms, and other CPU-bound tasks. I avoid them for simple async network work because fetch is already non-blocking. A good worker design has typed messages, cancellation or stale-result protection, error handling, and careful data transfer costs.

Detailed Interview Answer

Workers are a performance architecture tool.

They help when the bottleneck is CPU work on the main thread.

Examples:

large JSON processing
client-side search indexes
syntax highlighting
markdown parsing
image processing
compression
diff calculation
large data normalization

They do not make all code faster.

They make the UI thread freer.

Main Thread vs Worker

The main thread owns UI.

Workers do not directly access:

DOM
window UI APIs
React state
layout measurements
document

Workers communicate through messages.

This boundary is important.

Basic Worker Contract

Define message types.

type WorkerRequest =
  | { type: "buildIndex"; requestId: string; documents: string[] }
  | { type: "search"; requestId: string; query: string };

type WorkerResponse =
  | { type: "indexReady"; requestId: string }
  | { type: "searchResult"; requestId: string; results: string[] }
  | { type: "error"; requestId: string; message: string };

Typed contracts prevent mismatched messages.

The boundary is runtime, so still validate if messages may come from untrusted sources.

Stale Result Protection

Worker responses can arrive late.

let latestRequestId = "";

function runSearch(query: string) {
  const requestId = crypto.randomUUID();
  latestRequestId = requestId;
  worker.postMessage({ type: "search", requestId, query });
}

worker.onmessage = (event: MessageEvent<WorkerResponse>) => {
  if (event.data.requestId !== latestRequestId) {
    return;
  }

  renderResults(event.data);
};

This prevents older searches from overwriting newer results.

Data Transfer Cost

Worker communication has cost.

The browser usually copies data through structured cloning.

For large binary data, use transferable objects.

worker.postMessage({ buffer }, [buffer]);

Transferable objects move ownership instead of copying.

That can be much faster for large buffers.

Cancellation

Workers need cancellation or stale-result handling.

Options:

terminate and recreate worker
send cancel message
ignore stale request id
use AbortSignal where supported by internal work
chunk work and check cancellation flag

Ignoring stale results is often simple and effective for search-like UI.

Terminating is stronger but more expensive.

Error Handling

Worker errors need a UI strategy.

worker.onerror = () => {
  showFallbackSearch();
};

worker.onmessageerror = () => {
  showFallbackSearch();
};

The app should not silently fail if a worker crashes.

TypeScript Worker Setup

Workers often need separate typing because they run in a different environment.

Watch for:

DOM types vs WebWorker types
module worker support
bundler-specific worker imports
shared message contracts
worker test setup

Keep shared message types in a module imported by both sides.

Avoid importing React or server-only utilities into worker code.

When Workers Are Not Needed

Do not use workers for everything.

Workers are usually unnecessary for:

normal fetch calls
small data transforms
simple debounce
CSS animations
server-rendered content
rare tasks that finish quickly

They add lifecycle, bundling, messaging, and testing complexity.

React Integration

In React, isolate worker interaction inside a hook or service.

type SearchState =
  | { status: "idle" }
  | { status: "working" }
  | { status: "success"; results: string[] }
  | { status: "error"; message: string };

The component should not need to know every worker detail.

It should render typed state.

Common Mistakes

Common mistakes include:

moving network work to a worker unnecessarily
sending huge objects repeatedly
forgetting stale responses
not handling worker errors
creating many workers per interaction
importing client UI code into worker modules
assuming workers can touch the DOM

The most common senior-level miss is ignoring transfer and lifecycle cost.

Senior Trade-Offs

Workers improve responsiveness but add architecture.

They are worth it when:

CPU work blocks input
work can be isolated from DOM
data transfer cost is acceptable
results can be asynchronous
fallback behavior is clear

They are less useful when the bottleneck is server latency, network waterfall, or layout.

Interview Framing

In an interview, say:

I use workers when CPU-bound JavaScript blocks the main thread. I design typed request and response messages, protect against stale results, handle errors, and account for cloning or transfer cost.

Revision Notes

  • Workers run JavaScript off the main thread.
  • They cannot directly access the DOM.
  • Message contracts should be typed.
  • Large data transfer can be expensive.
  • Stale result handling is essential.
  • Workers help CPU bottlenecks, not every async problem.

Follow-Up Questions

  • When would a worker improve INP?
  • Why can data transfer to a worker be expensive?
  • How do you handle stale worker responses?
  • What APIs are unavailable inside a worker?
  • Why not move every fetch call into a worker?

How I Would Answer This In A Real Interview

I would say workers are useful when CPU-heavy JavaScript blocks the main thread and hurts responsiveness. I would isolate the worker behind a typed message contract, protect against stale results, handle worker crashes, and measure whether data transfer cost is worth it. I would not use a worker for ordinary network requests because the browser already handles those asynchronously.