What is a Web Worker — and why await doesn't stop your page from freezing

Here's a familiar scene. You hit "Process" on a page with a big file. The spinner freezes. The "Cancel" button won't click. You can't even select text. Five seconds later everything comes back to life at once.
The annoying part: the function was async, and you honestly added await. It didn't help. Let's see why — and what actually does.
Why the page freezes
All your page's JavaScript runs on one thread — the main thread. The browser also uses that same thread to draw the page and handle clicks.
Picture a café with one waiter. He takes orders, serves dishes and counts the till. While he spends five minutes counting cash, nobody can reach him. The room is "frozen".
So what does await do? It's great when you're waiting — for a server reply, a timer, a file. While the wait goes on, the thread is free. More on that in the article about async.
But a loop over a million rows isn't waiting, it's work. Somebody still has to do it. And that somebody is the same single waiter. Putting async before a function doesn't hire a second one.
A Web Worker is a second thread
A Web Worker is a separate JavaScript file that the browser runs on its own thread, next to the main one. It's a second cook in the kitchen. He can knead dough as long as he likes while the waiter keeps serving the room.
You create one in a single line. Here's how it looks in a Vite project:
// main.js — the page
const worker = new Worker(new URL("./worker.js", import.meta.url), { type: "module" });
worker.postMessage(bigList); // hand off the work
worker.onmessage = (e) => {
console.log("done:", e.data); // get the result back
};
// worker.js — the second thread
self.onmessage = (e) => {
const result = heavySort(e.data); // even 5 seconds — the page stays alive
self.postMessage(result);
};
All communication goes through messages: postMessage one way, onmessage back. Like notes passed through the kitchen hatch.
Three things that surprise everyone
1. A worker has no page. There's no document and no window inside it. It can't change a button or text on its own — the DOM lives only on the main thread. The worker computes and returns a result; the page does the drawing.
2. Data is copied, not shared. When you send an array to a worker, the browser makes a full copy. Change the copy in the worker and nothing changes on the page. And copying a huge array takes time — sometimes longer than the work itself.
3. You can swap the copy for a handoff. Binary data (ArrayBuffer) has a trick: you can hand it over outright.
worker.postMessage(buffer, [buffer]);
console.log(buffer.byteLength); // 0 — the page no longer has the buffer
No copying, the transfer is instant. But the sender is left with an empty 0-byte buffer. If the page reads it later, you'll get zeros and spend a long time hunting the bug.
4. A worker doesn't leave on its own. A thread lives until someone stops it. A common mistake is creating new Worker(...) on every click. A hundred clicks later you have a hundred threads and the tab is eating memory. Create the worker once and reuse it. When you no longer need it — worker.terminate().
When you need a worker and when you don't
A worker isn't free. Starting a thread and passing data around cost time too. A rule of thumb:
- Yes, if the operation visibly stalls the UI: parsing a big CSV, processing photos, compression, searching tens of thousands of records.
- Yes for AI right in the browser. Libraries like Transformers.js recommend running the model in a worker — otherwise the page stalls while it answers.
- No for server requests. That's waiting, and
awaithandles it. - No if the task takes 20 milliseconds. It's often cheaper to speed it up — say, remove a nested loop. How to spot that is in the article on Big O.
How to check the main thread is the problem: open DevTools → Performance and record the click. A long grey bar with a red corner is a Long Task. Anything over 50 ms feels like lag to the user.
Are a Web Worker and a Service Worker the same thing?
No. A Web Worker serves your page for heavy computing and lives while the tab is open. A Service Worker sits between your site and the network: it caches files and makes the site work offline.
Can a worker make a fetch request?
Yes. Workers have fetch, timers, IndexedDB and WebSocket. What they lack is anything tied to the page: the DOM, window, localStorage.
How do I ask AI to move code into a worker?
Be specific: "move function X into a Web Worker, the page sends data via postMessage, pass ArrayBuffers via transfer". Without that, AI often just adds async — which, as we saw, doesn't help.
Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.





