How to use Web Workers to protect the Main Thread and prevent frozen UIs.
Timothy clicked the "Export Report" button.
On the screen, a small loading spinner appeared. But it wasn't spinning. It was frozen solid. Timothy tried to click another tab on the page, but the entire browser window was unresponsive.
Ten seconds later, the UI suddenly unfroze, and the file downloaded.
"It works," Timothy said, "but the application completely dies while it's processing the data."
Margaret pulled up a chair. "You have built a beautiful kitchen, Timothy. But you only have one chef. If you ask him to chop ten thousand onions, he cannot also greet the customers."
The Single Thread
Margaret opened the performance tab and pointed to a massive, solid yellow block taking up the timeline.
"JavaScript is single-threaded," Margaret explained. "We call it the Main Thread, but you should think of it as the UI Thread. Its primary job is to paint the screen, run animations, and listen for clicks."
Timothy pointed to his code. "But I used async and await! I thought that made it non-blocking."
"Async is for waiting," Margaret corrected. "When you await a network request, the chef puts the soup on the stove and walks away to do other things. But data processing—like formatting a massive CSV or doing heavy math—is active work. The chef is chopping. He cannot step away."
The Clone
"So how do I process the data without freezing the screen?" Timothy asked.
"You hire a sous-chef," Margaret said. "You create a Web Worker."
Margaret created a brand new file in their project called worker.js.
"A Web Worker isn't just a helper—it is a literal clone of the JavaScript engine, running in parallel with the original," she explained. "The sous-chef works in a dark room. He cannot see the menu board (the DOM) or touch the tables (the UI elements), but he can still use tools like fetch() and setTimeout()."
Inside worker.js, she wrote a simple listener:
// worker.js - The Sous-Chef's Room
self.onmessage = function(event) {
const rawData = event.data;
// The Chef is chopping the onions in the background
const processedCSV = heavyDataProcessing(rawData);
// Send the finished product back to the kitchen
self.postMessage(processedCSV);
};
The Dispatch
Margaret switched back to the main application file.
"The Main Thread and the Worker Thread live in completely different worlds," she said. "They cannot share variables. They can only communicate by passing notes under the door."
She updated Timothy's export function, making sure to handle the case where the sous-chef might make a mistake.
// main.js - The Kitchen (UI Thread)
const exportButton = document.getElementById('export-btn');
exportButton.addEventListener('click', () => {
// 1. Show the spinning UI
showLoadingSpinner();
// 2. Hire the sous-chef
const worker = new Worker('worker.js');
// 3. Listen for the note back under the door
worker.onmessage = function(event) {
const processedCSV = event.data;
downloadFile(processedCSV);
hideLoadingSpinner();
// Fire the sous-chef so he doesn't consume memory
worker.terminate();
};
// 4. Handle emergencies in the kitchen
worker.onerror = function(error) {
console.error("Sous-chef had a breakdown:", error);
hideLoadingSpinner();
showErrorMessage();
worker.terminate();
};
// 5. Slide the raw data under the door
const rawData = getMassiveDataset();
worker.postMessage(rawData);
});
Timothy ran the code again. He clicked the "Export Report" button.
This time, the loading spinner spun beautifully. He could click other buttons. He could highlight text. The UI was perfectly fluid. Ten seconds later, the file downloaded.
The Architectural Divide
"This changes everything," Timothy said, watching the smooth animation.
"It forces you to think differently," Margaret agreed. "Junior developers put everything on the Main Thread and hope the computers are fast enough to hide it."
"Senior developers protect the Main Thread at all costs. They treat it purely as a presentation layer. If a task takes more than 50 milliseconds of pure CPU time, they hand it off to a Worker."
Senior Tip: Transferable Objects
Passing notes under the door (postMessage) creates a copy of your data. For massive datasets, copying takes time. You can use Transferable Objects (like ArrayBuffer) to literally hand the memory directly to the Worker without copying it, resulting in an instant handoff with zero overhead.
The Spinner
Timothy watched the spinner go. The chef was finally free to manage the restaurant—greeting customers, answering the phone, and keeping the place alive—while the sous-chef quietly chopped onions in the back.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Top comments (8)
I enjoyed your post, there was just one thing that caught my eye. I found this:
To be a bit misleading, because in most cases the main thread does not wait while the browser handles tasks. In a way, web workers are also async, since JS main thread continues to run while the browser handles the computation and gets back when it is completed. So, you see, all these are async and JS main thread does not wait.
Gass, this is a fantastic technical distinction and you are absolutely right. 💯
When Margaret says "Async is for waiting," it is definitely a pedagogical simplification. As you correctly pointed out, the Main Thread never actually stops and "waits"—it immediately yields control back to the Event Loop to handle other UI tasks while the browser's background APIs handle the network request. And, just as you noted, Web Worker message passing is also fundamentally asynchronous.
The specific distinction Margaret was trying to draw for Timothy was between I/O-bound tasks (where the system is just waiting for an external response) versus CPU-bound tasks (where a JavaScript engine has to actively crunch numbers and lock up its thread).
But your clarification is incredibly valuable. The Main Thread keeps moving in both scenarios. Thanks for keeping the technical rigor high in the comments! ❤🙏
A okay, now I see your point. Very interesting. Even though the heavy computation is given to the browser C++ engine it can still freeze the JS V8 main thread due to the CPU limitations. The CPU becomes the bottleneck in this scenario.
🙏❤✨
Brilliant analogy of chefs and threads, made complex concepts so simple.
Loved how Web Workers were explained as sous‑chefs in action.
This piece truly elevates understanding of JavaScript’s hidden power.
🙏🌹✨❤️
The rendering optimization angle is interesting — most JS perf issues I've seen in production aren't about algorithm complexity but about unnecessary re-renders and missed memoization boundaries. The tooling has gotten better at surfacing this but the mental model of 'when does this actually run' still takes time to build.
Hi member_fc281ffe! Yep. We spend so much time agonizing over Big O notation, but in the real world, the bottleneck is almost always framework overhead, unnecessary re-renders, or blocking the Main Thread. Building that exact mental model of "when and where does this actually run" is the entire goal of this series. Thanks for the great insight! 💯❤🙏