Web Workers
Objectives
By the end of this chapter, you should be able to:
- Explain why JavaScript sometimes needs a web worker
- Create a worker and send it messages with
.postMessage() - Receive messages back with
.onmessage
๐ก Why this matters: JavaScript in a page runs on a single thread, the same one responsible for rendering the page and responding to clicks. A slow calculation on that thread doesn’t just take a while, it freezes everything else too. Workers give heavy work somewhere else to run.
โ ๏ธ A note on verification: the same limitation as the last few lessons applies. Web workers need a real browser, running actual separate script files, which isn’t something this sandbox can execute the way
nodeverified earlier modules. The code below is accurate, based on the standardized Web Workers API, run it yourself in a browser to see it firsthand.
What You’re Building
A sales data analyzer: click one button to crunch a large list of numbers, and watch a separate, constantly-ticking counter on the same page to prove it never freezes while that work happens.
data-analyzer/
โโโ index.html
โโโ script.js
โโโ analyzer-worker.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sales Data Analyzer</title>
</head>
<body>
<h1>Sales Data Analyzer</h1>
<button id="analyzeBtn">Analyze Sales Data</button>
<p id="result"></p>
<p id="liveCounter">Page is responsive: 0</p>
<script src="script.js"></script>
</body>
</html>
analyzer-worker.js is never linked with a <script> tag, workers are loaded through new Worker(...) in JavaScript instead, covered in Step 3. Everything else from here on goes in script.js, except where noted.
Step 1: See the Problem First
Before reaching for a worker, look at what it’s solving. A heavy, synchronous loop blocks the one thread everything else runs on too, including the ticking counter below.
let seconds = 0;
setInterval(() => {
seconds++;
document.getElementById("liveCounter").textContent = `Page is responsive: ${seconds}`;
}, 1000);
// If analyzeBtn ran this directly, on the main thread:
function analyzeSynchronously(numbers) {
let total = 0;
for (let i = 0; i < numbers.length; i++) {
total += numbers[i];
}
return total;
}
If analyzeSynchronously() took three seconds to churn through a huge array, liveCounter would freeze for those three seconds too, no ticking, no button clicks registering, nothing. The loop and the counter share the same thread, one blocks the other.
Step 2: Create the Worker File
A web worker runs JavaScript in a genuinely separate thread, with its own memory, unable to touch the DOM directly. It’s created from a separate file, analyzer-worker.js, sitting alongside index.html in the file structure above.
// analyzer-worker.js
self.onmessage = (event) => {
const numbers = event.data;
let total = 0;
let max = numbers[0];
for (let i = 0; i < numbers.length; i++) {
total += numbers[i];
if (numbers[i] > max) max = numbers[i];
}
self.postMessage({
total,
average: total / numbers.length,
max,
});
};
self.onmessage is how the worker receives data, event.data is whatever was sent to it. Once the calculation finishes, self.postMessage() sends the result back, an object with total, average, and max, in this case.
Step 3: Wire Up the Button
Back in script.js:
const analyzeBtn = document.getElementById("analyzeBtn");
const result = document.getElementById("result");
analyzeBtn.addEventListener("click", () => {
const salesData = Array.from({ length: 5_000_000 }, () => Math.random() * 1000);
result.textContent = "Analyzing...";
const worker = new Worker("analyzer-worker.js");
worker.postMessage(salesData);
worker.onmessage = (event) => {
const { total, average, max } = event.data;
result.textContent = `Total: ${total.toFixed(2)}, Average: ${average.toFixed(2)}, Max: ${max.toFixed(2)}`;
worker.terminate();
};
});
new Worker("analyzer-worker.js") loads that separate file and starts it running on its own thread. Clicking “Analyze Sales Data” sends five million numbers to it with .postMessage(). While it crunches through them, liveCounter above keeps ticking every second, completely unaffected, exactly the outcome Step 1 couldn’t achieve. When the result comes back through worker.onmessage, #result updates and .terminate() shuts the worker down, its job is done.
Step 4: What the Worker Can’t Do
Notice analyzer-worker.js never touches document, workers have no DOM access at all, no window, either. Communication only happens through messages, .postMessage() and .onmessage, in both directions. This is deliberate: if a worker could freely modify the DOM from its own thread at the same time the main thread does, the two could conflict in ways that are extremely hard to reason about. Message passing avoids that entirely, which is why the worker computes the numbers and hands back a plain result object, rather than trying to update #result itself.
The Complete script.js
let seconds = 0;
setInterval(() => {
seconds++;
document.getElementById("liveCounter").textContent = `Page is responsive: ${seconds}`;
}, 1000);
const analyzeBtn = document.getElementById("analyzeBtn");
const result = document.getElementById("result");
analyzeBtn.addEventListener("click", () => {
const salesData = Array.from({ length: 5_000_000 }, () => Math.random() * 1000);
result.textContent = "Analyzing...";
const worker = new Worker("analyzer-worker.js");
worker.postMessage(salesData);
worker.onmessage = (event) => {
const { total, average, max } = event.data;
result.textContent = `Total: ${total.toFixed(2)}, Average: ${average.toFixed(2)}, Max: ${max.toFixed(2)}`;
worker.terminate();
};
});
Try It
These can’t run in this course’s tooling, write the code and, if you have a moment, try it in an actual browser with two files.
- Build
analyzer-worker.jsand theanalyzeBtnhandler from Steps 2 and 3. - Add the
liveCounterinterval from Step 1 running alongside it, and reason through why it would keep ticking during the analysis instead of freezing. - Change the worker to also compute the minimum value, sending it back as part of the same result object, and update
#resultto display it.
Recap
- JavaScript in a page runs on one thread. A heavy, synchronous computation on that thread freezes everything sharing it, including something as simple as a ticking counter,
async/timers alone don’t fix this, they only manage when code runs, not what thread it’s on. new Worker("file.js")runs that file on a separate thread..postMessage()sends data to it,.onmessagereceives data back, on both sides, this lesson’s analyzer sent a huge array in and got a summary object back.- A worker has no DOM access, communication only happens through message passing.
.terminate()stops a worker once its job is done. Reach for a worker specifically when a computation is heavy enough to freeze the page, not for ordinary async work.
Next lesson: a first look at the Canvas API, drawing directly onto a page.