In short
The HTML Web Workers API runs JavaScript in a background thread, so heavy work like big calculations doesn't freeze your page. In this lesson you will see the freezing problem for yourself, create a worker file, send messages back and forth, stop a worker, and learn what workers can and cannot do.
The HTML Web Workers API runs JavaScript in a background thread, so heavy work like big calculations doesn't freeze your page.
In this lesson you will see the freezing problem for yourself, create a worker file, send messages back and forth, stop a worker, and learn what workers can and cannot do.
Why use Web Workers? Heavy work blocks the page
Normally all JavaScript on a page runs on one thread, called the main thread. A thread is like a single cashier in a shop: it serves one customer at a time. The main thread also handles clicks, scrolling and drawing the page. If your script gives it a huge task, every other "customer" waits. The page freezes: buttons don't respond and animations stop.
Try this page. It counts the prime numbers below 20 million (a prime is a number that can only be divided by 1 and itself):
<button type="button" id="count">Count primes</button>
<button type="button" onclick="this.textContent = Number(this.textContent) + 1">0</button>
<p id="result"></p>
<script>
function countPrimes(limit) {
let count = 0;
for (let n = 2; n < limit; n++) {
let isPrime = true;
for (let d = 2; d * d <= n; d++) {
if (n % d === 0) { isPrime = false; break; }
}
if (isPrime) count++;
}
return count;
}
document.querySelector("#count").addEventListener("click", () => {
const total = countPrimes(20000000);
document.querySelector("#result").textContent = `Found ${total} primes.`;
});
</script>
Click "Count primes", then try to click the second button (a simple click counter). Nothing happens for several seconds. The page is frozen until the calculation ends. (n % d gives the remainder of dividing n by d; a remainder of 0 means it divides evenly.)
A Web Worker is a second cashier. It runs your heavy script on a separate thread, and the main thread stays free for the user.
How to create a Web Worker
A worker lives in its own JavaScript file. The page and the worker talk by sending messages, like two people passing notes through a window.
Create primes-worker.js in the same folder as your page:
// primes-worker.js
self.onmessage = (event) => {
const limit = event.data;
let count = 0;
for (let n = 2; n < limit; n++) {
let isPrime = true;
for (let d = 2; d * d <= n; d++) {
if (n % d === 0) { isPrime = false; break; }
}
if (isPrime) count++;
}
self.postMessage(count);
};
selfmeans the worker itself (inside a worker there is nowindow).self.onmessageruns when the page sends a message. The message is inevent.data: here, the limit number.- The loop is the same prime counter as before.
self.postMessage(count)sends the result back to the page.
Now the page, primes.html:
<button type="button" id="count">Count primes</button>
<button type="button" onclick="this.textContent = Number(this.textContent) + 1">0</button>
<p id="result" role="status"></p>
<script>
const result = document.querySelector("#result");
const worker = new Worker("primes-worker.js");
worker.onmessage = (event) => {
result.textContent = `Found ${event.data} primes.`;
};
worker.onerror = (error) => {
result.textContent = `Worker error: ${error.message}`;
};
document.querySelector("#count").addEventListener("click", () => {
result.textContent = "Counting in the background…";
worker.postMessage(20000000);
});
</script>
new Worker("primes-worker.js")starts the worker from its file.worker.onmessageruns when the worker sends something back;event.datais the count.worker.onerrorshows a message if the worker script crashes.worker.postMessage(20000000)sends the limit to the worker, and the page carries on at once.
Click "Count primes", then click the counter button: it keeps counting while the primes are calculated. After a few seconds you will see "Found 1270607 primes."
Common mistake: Opening the page with
file://. Chrome and Edge block workers loaded from files on disk, and you get a security error. Put both files inhtdocsand open them throughhttp://localhost/, or use VS Code Live Server.
Sending messages: what can you send?
postMessage() can send numbers, strings, arrays, plain objects, dates, Blobs and files. The data is copied, not shared, so changing it on one side does not change the other side. You cannot send functions or HTML elements.
A common pattern is to send an object with a "type", so the worker knows what job to do:
// In the page
worker.postMessage({ task: "primes", limit: 5000000 });
// In the worker
self.onmessage = (event) => {
const { task, limit } = event.data;
if (task === "primes") {
// ...count primes up to limit...
}
};
const { task, limit } = event.data is a shortcut that copies the task and limit properties into two variables.
Stopping a worker with terminate()
A worker keeps running until you stop it. There are two ways:
| Method | Called from | What it does |
|---|---|---|
worker.terminate() | The page | Stops the worker at once, even in the middle of a task |
self.close() | Inside the worker | The worker stops itself |
Add a Cancel button to the page:
<button type="button" id="cancel">Cancel</button>
<script>
document.querySelector("#cancel").addEventListener("click", () => {
worker.terminate();
result.textContent = "Cancelled.";
});
</script>
Put this inside the same page as the worker example (the worker and result variables must already exist). After terminate() the worker is gone for good; to count again, create a new one with new Worker(...).
What can Web Workers access and not access?
Workers are separated from the page on purpose, which keeps them fast and safe.
| Workers CAN use | Workers CANNOT use |
|---|---|
fetch() to load data | The DOM: document, elements, querySelector |
setTimeout and setInterval | The window object |
| IndexedDB (browser database) | localStorage and sessionStorage |
| WebSockets | alert(), confirm(), prompt() |
importScripts() or modules to load other code | Variables from the page's scripts |
navigator (some parts) and location (read only) | The page's CSS and styles |
So a worker does the thinking and the page does the showing. When the worker finishes, it sends the result back, and the page updates the HTML.
Tip: You can write a worker as a JavaScript module with
new Worker("worker.js", { type: "module" }). Then you can useimportinside the worker, which is the modern way to load other files.
Good uses for workers include processing large data files, resizing or filtering images, heavy maths, parsing big JSON, and searching through long lists. For small tasks, a worker is not worth it: starting one and sending messages takes time too.
Practice exercise
Build a "Big sum" page with a number input and a button. When clicked, a worker adds up all whole numbers from 1 to the chosen number and sends back the total. Show "Working…" while it runs and add a Cancel button that terminates the worker.
Hint: In the worker, use a for loop and a total variable, then self.postMessage(total). Convert the input value with Number(input.value) before sending it. After terminating, create a new worker for the next click.
Key takeaways
- JavaScript normally runs on one main thread; heavy work there freezes the page.
- A Web Worker runs a separate script file on a background thread:
new Worker("file.js"). - The page and worker talk with
postMessage()andonmessage; the data is inevent.dataand is copied. - Stop a worker with
worker.terminate()from the page orself.close()inside it. - Workers cannot touch the DOM,
windoworlocalStorage, and must be loaded through a server, notfile://.