Skip to main content
TF
By Rohit V.8 min readArticle

Web Workers Explained: When They Actually Help

TF
ToolsFuel Team
Web development tools & tips
Server hardware racks lit in blue

Photo by Unsplash on Unsplash

Short Answer

> Quick answer: A Web Worker runs JavaScript on a separate thread, so long computations stop freezing the page. It has no access to the DOM, no `window`, and it talks to the main thread only through messages. That means a worker helps exactly one problem: a long synchronous block of your own JavaScript. It does nothing for slow network calls, slow rendering, large images or heavy CSS.

The test is simple. Open the performance profiler, record the slow moment, and look for a single long task in the main thread flame chart. If there's one yellow block of scripting lasting hundreds of milliseconds, a worker will help. If the time is spread across layout, paint or waiting on the network, a worker will change nothing and cost you an architecture.

What a Worker Actually Is

The browser gives your page one main thread. It parses HTML, runs your JavaScript, computes styles, lays out the page, paints it, and handles every click and scroll. All of that shares the same queue, so any one of them monopolising the thread stops the rest.

That's why a two second loop freezes the page. The click handler you registered is sitting in the queue waiting for the loop to finish, and the browser cannot repaint either, so the interface stops responding to everything at once.


A worker is a second thread with its own JavaScript environment. You start one with `new Worker('worker.js')` and the file runs in parallel. Nothing in it can block your page, because it isn't on the page's thread.


The price of that isolation is real. There's no `document`, no `window`, no direct access to any variable in your main script. The worker gets `fetch`, timers, `IndexedDB`, `WebAssembly` and most of the language, and that's roughly the deal.


Communication is by message passing. You call `worker.postMessage(data)`, the worker receives it in an `onmessage` handler, and it replies the same way. The data is copied using the structured clone algorithm, the same one behind
structuredClone, which is why you can send a `Map` but not a function.

The Long Task Test

Before writing a worker, find out whether you have the problem workers solve. This takes about two minutes and saves a lot of wasted work.

Open devtools, go to the Performance panel, hit record, do the slow thing, stop. The main thread track shows what the browser spent time on. Anything over 50 milliseconds gets flagged as a long task, and a red triangle marks it.


Now look at what colour the long block is. Yellow is scripting, which is your JavaScript, and that's the case a worker can move. Purple is style and layout, green is paint, and neither of those can go to a worker at all, because they're the browser's own rendering work and it only happens on the main thread.


If the timeline is mostly empty with a long gap, you're waiting on the network, and the fix is caching, a smaller payload or a CDN. Our
Core Web Vitals guide covers which metric each of these shows up in, since a frozen page usually damages INP rather than LCP.

I've watched people reach for a worker because a page felt slow, move a 40 millisecond function into it, and measure no change at all. The profiler would have told them in advance.

The Cases Where Workers Genuinely Win

Parsing or transforming a large file in the browser. A big CSV or a multi megabyte JSON blob takes real time to walk, and doing it on the main thread freezes everything until it finishes. In a worker the page stays interactive and you can show progress.

Image and video processing. Anything touching pixel data through a canvas, resizing, filtering, or generating thumbnails, is genuinely expensive. `OffscreenCanvas` lets a worker draw without the main thread involved at all, which is the strongest version of this case.


Crypto and hashing. Hashing a large file, deriving a key, or anything deliberately slow by design belongs off the main thread. The whole point of a slow key derivation is that it takes time, so blocking the interface with it is the wrong outcome. Our
hashing explainer covers why some of these are intentionally expensive.

Search and filtering over a large in memory dataset. Ten thousand rows filtered on every keystroke is a long task per keypress, and moving it to a worker is what makes the input feel instant.


WebAssembly workloads. Anything compute heavy enough to justify Wasm is almost certainly heavy enough to justify a worker around it.

The Costs Nobody Mentions Up Front

Starting a worker isn't free. Spawning the thread and loading its script takes time, typically tens of milliseconds, and it has its own memory. For work that takes 20 milliseconds, the startup alone costs more than you save.

Message passing copies data. Sending a 50 megabyte array to a worker means serializing it, allocating a copy, and deserializing it, and that copy happens on the main thread. It's entirely possible to build a worker that makes the page slower because the transfer costs more than the computation did.


The escape from that is transferable objects. Pass an `ArrayBuffer` in the transfer list and ownership moves instead of the bytes being duplicated, which is close to instant regardless of size. The original becomes unusable in the sending thread, which is the trade.


Debugging is harder. The worker has its own context in devtools, errors surface differently, and a stack trace crossing the message boundary just stops. Not impossible, but slower than debugging normal code.


And the code splits in two. A function that was one call becomes a message, a handler, a reply and a promise wrapper. Libraries like Comlink hide most of that behind a proxy, which is worth reaching for if you end up with more than a couple of worker calls. The
MDN worker guide has the raw API if you'd rather not add a dependency.

Workers, Service Workers and Worklets Are Different Things

The naming causes more confusion than the concepts do, so it's worth separating them cleanly.

A Web Worker, sometimes called a dedicated worker, belongs to one page and dies when that page closes. It exists to move computation off the main thread. That's the one this article is about.


A Service Worker is a network proxy. It sits between your page and the network, intercepts requests, serves things from a cache, and keeps running after the page is closed. It's what makes offline mode and push notifications possible. It is not a place to put heavy computation, and its lifecycle is aggressive about shutting down between events.


A Shared Worker is a worker that several tabs of the same origin can talk to at once, useful for coordinating state across tabs. Support is decent but it's genuinely rare in the wild.


Worklets are narrow, high performance hooks into specific parts of the rendering pipeline, like audio processing or custom paint. They're not general purpose and you'd know if you needed one.


If the goal is a page that stops freezing, it's a dedicated worker. If the goal is a page that works offline, it's a service worker, and the two solve problems that have nothing to do with each other.


There's a fifth thing people sometimes lump in here, and it isn't a worker at all. `requestIdleCallback` and `scheduler.yield()` both run on the main thread, they just break a long task into smaller pieces so the browser can slip a repaint or a click handler in between. That's often the cheaper fix. If your expensive work is a loop over ten thousand items rather than one indivisible computation, yielding every few hundred iterations keeps the page responsive without any of the message passing, the copying or the split codebase that a worker brings.


I've reached for that first more often than for a worker, and it solved the problem outright maybe half the time. The rule I use now is that a worker earns its complexity when the work is genuinely indivisible or genuinely long, tens of milliseconds you can chunk is a scheduling problem, not a threading one.

Frequently Asked Questions

Can a Web Worker access the DOM?

No. There is no document or window inside a worker, which is what makes it safe to run in parallel. It sends results back to the main thread by message, and the main thread updates the DOM.

Will a Web Worker make my slow page faster?

Only if the slowness is a long block of your own JavaScript. Record the Performance panel and look for a long yellow scripting task. Time spent in layout, paint or waiting on the network cannot move to a worker.

What is the difference between a Web Worker and a Service Worker?

A Web Worker moves computation off the main thread for one page. A Service Worker is a network proxy that caches requests and enables offline use, and it keeps running after the page closes. They solve unrelated problems.

Is sending data to a worker expensive?

Yes for large payloads, because the data is copied using the structured clone algorithm. Use the transfer list for ArrayBuffers to move ownership instead of copying, which is near instant. See our [structuredClone guide](/blog/structuredclone-vs-json-deep-copy-javascript-2026) for what clones and what does not.

How many workers should I create?

Usually one, reused, rather than one per task, because each spawn costs time and memory. For genuinely parallel work, navigator.hardwareConcurrency gives a sensible upper bound to size a pool against.

Try ToolsFuel

23+ free online tools for developers, designers, and everyone. No signup required.

Browse All Tools