JavaScript's single-threaded main thread is also your UI thread — any heavy computation you run on it blocks rendering and input handling at the same time, which is exactly the problem Web Workers exist to solve.
Web Workers let you run JavaScript on a separate thread, off the main thread, communicating with your application via message passing rather than shared memory. Heavy computation — data processing, complex calculations, parsing large files — can run in a worker without blocking the UI, keeping the page responsive to user input throughout.
Why Web Workers Matter (and When to Skip Them)
Any sufficiently heavy synchronous JavaScript task on the main thread directly hurts INP (Interaction to Next Paint) — the page can't respond to clicks or input while that task runs. Web Workers move that work off the main thread entirely, letting the UI stay responsive regardless of how long the computation takes.
Skip Web Workers for lightweight computation that completes in a few milliseconds — the overhead of setting up a worker and message-passing isn't worth it for work that wouldn't meaningfully block the main thread anyway. They're a tool for genuinely heavy, blocking computation specifically.
Getting Started with Web Workers
Creating a worker file:
// worker.ts
self.onmessage = (e: MessageEvent) => {
const result = heavyComputation(e.data);
self.postMessage(result);
};
function heavyComputation(input: number[]) {
return input.reduce((sum, n) => sum + Math.sqrt(n), 0);
}
Using it from the main thread:
const worker = new Worker(new URL("./worker.ts", import.meta.url));
worker.postMessage(largeArray);
worker.onmessage = (e) => {
console.log("Result:", e.data);
};
In React, wrapping worker communication in a hook:
function useWorkerResult(input: number[]) {
const [result, setResult] = useState<number | null>(null);
useEffect(() => {
const worker = new Worker(new URL("./worker.ts", import.meta.url));
worker.postMessage(input);
worker.onmessage = (e) => setResult(e.data);
return () => worker.terminate();
}, [input]);
return result;
}
Core Web Workers Concepts Every Developer Should Know
Workers communicate via message passing, not shared memory. Data sent to and from a worker is copied (structured clone), not shared by reference — this means large data transfers have a real serialization cost, and mutating an object in the worker doesn't affect the main thread's copy.
Workers don't have access to the DOM. They can't directly manipulate the page — all UI updates still need to happen on the main thread after receiving a result from the worker via postMessage. Workers are for computation, not rendering.
Transferable objects avoid the copy cost for large binary data. For large ArrayBuffers (like image or audio data), transferring ownership instead of cloning avoids the serialization overhead entirely:
worker.postMessage(largeBuffer, [largeBuffer]); // transferred, not copied
Worker lifecycle needs explicit management. Workers don't automatically clean themselves up — call worker.terminate() when it's no longer needed (like on component unmount) to avoid leaking resources.
Common Web Workers Mistakes and How to Fix Them
Mistake 1: using a worker for lightweight computation, adding message-passing overhead for work that wouldn't have meaningfully blocked the main thread anyway. Fix: reserve workers for genuinely heavy, main-thread-blocking computation, measured rather than assumed.
Mistake 2: not terminating workers, leaking resources over the application's lifetime. Fix: explicitly terminate workers when they're no longer needed, especially in component cleanup functions.
Mistake 3: transferring large data without using Transferable objects, paying an unnecessary clone cost for large buffers. Fix: use transferable objects for large binary data where ownership transfer (not continued main-thread access) is acceptable.
When Should You Use Web Workers Instead of Just Optimizing the Algorithm?
Use Web Workers when the computation is genuinely heavy and can't be meaningfully reduced through algorithmic improvements alone — they solve a threading problem, not an algorithmic complexity problem. Optimize the algorithm first if the computation is doing unnecessary work; only reach for a worker once the remaining necessary computation is still heavy enough to block the main thread noticeably.
Web Workers in Production
Measure actual main-thread blocking time before reaching for a worker — not all heavy-sounding computation is actually a problem in practice, and adding worker complexity without a measured benefit is unnecessary overhead. Also handle worker errors explicitly (worker.onerror), since an unhandled worker exception can fail silently without much information.
If a specific interaction in your app has poor INP due to a long synchronous computation on the main thread, that's the concrete case Web Workers are built to fix — move that specific computation off-thread first.