JavaScript on a page runs on one thread: while a long calculation is running, nothing else can happen. Buttons stop responding, animations freeze, and the page seems broken. A web worker is a second script that runs in the background on its own thread, so heavy work does not block the page. This lesson covers how to start one, how the page and the worker talk by passing messages, and what a worker cannot do.
You met this in JS Timers: a busy loop keeps everything else from running, including clicks and drawing. If a page spends two seconds crunching numbers, it is frozen for two seconds. Moving that work to a worker keeps the page responsive.
new Worker("worker.js")postMessage and receiving them with the message eventA worker usually lives in its own file. The page side looks like this (shown, not run, because it needs a second file):
// main page
const worker = new Worker("worker.js");
worker.onmessage = function (event) {
console.log("answer:", event.data);
};
worker.postMessage(21);
// worker.js
onmessage = function (event) {
postMessage(event.data * 2);
};
The worker file listens for a message, does its work, and sends a result back. On the page, event.data is whatever the worker sent.
To try a worker without making a second file, you can build the worker's code from a string. A Blob holds the text, and URL.createObjectURL turns it into an address the Worker can load:
const source = "onmessage = function (event) { postMessage(event.data * 2); };";
const blob = new Blob([source], { type: "text/javascript" });
const worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = function (event) {
console.log(event.data);
worker.terminate();
};
worker.postMessage(21);
Notice the order: worker.postMessage(21) returns at once, and the answer arrives later in onmessage. Like timers and fetch, a worker is asynchronous.
A good job for a worker is anything slow and self-contained. This worker counts the prime numbers below a limit, which is a loop that does a lot of arithmetic:
const source = `
onmessage = function (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++;
}
postMessage(count);
};
`;
const worker = new Worker(URL.createObjectURL(new Blob([source], { type: "text/javascript" })));
worker.onmessage = function (event) {
console.log(event.data + " primes below 100000");
worker.terminate();
};
worker.postMessage(100000);
Data sent between the page and the worker is copied, not shared. You can send numbers, strings, arrays, objects, dates, and maps, but not functions. Changing the object you sent never changes the worker's copy:
const source = "onmessage = function (event) { event.data.items.push(4); postMessage(event.data); };";
const worker = new Worker(URL.createObjectURL(new Blob([source], { type: "text/javascript" })));
const original = { items: [1, 2, 3] };
worker.onmessage = function (event) {
console.log(original.items.length, event.data.items.length);
console.log(event.data === original);
worker.terminate();
};
worker.postMessage(original);
document, so a worker cannot read or change elements. It sends results back and the page does the updatingwindow. The global object inside a worker is called selffetch, timers, JSON, Math, localStorage is not available (use messages), and importScripts("other.js") loads more codeconst source = `
postMessage([typeof document, typeof window, typeof self, typeof fetch]);
`;
const worker = new Worker(URL.createObjectURL(new Blob([source], { type: "text/javascript" })));
worker.onmessage = function (event) {
console.log(event.data);
worker.terminate();
};
If the worker throws an error you did not catch, the page's onerror handler runs. You stop a worker from the page with worker.terminate() (it stops at once), or from inside with close(). Always stop workers you no longer need, because each one uses memory:
const source = "throw new Error('boom');";
const worker = new Worker(URL.createObjectURL(new Blob([source], { type: "text/javascript" })));
worker.onerror = function (event) {
event.preventDefault();
console.log("worker failed:", event.message.includes("boom"));
worker.terminate();
};
This demo shows the point of workers. The number keeps counting up on its own. Press Run in worker and the counter keeps going while the worker works. Then press Run on page and the counter freezes until the calculation finishes:
<p>Ticker: <strong id="tick">0</strong></p>
<button id="w">Run in worker</button>
<button id="p">Run on page</button>
<p id="result"></p>
<script>
let ticks = 0;
setInterval(function () { document.getElementById("tick").textContent = ++ticks; }, 100);
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.getElementById("p").addEventListener("click", function () {
document.getElementById("result").textContent = "Page result: " + countPrimes(4000000);
});
document.getElementById("w").addEventListener("click", function () {
const source = "onmessage = function (e) { " + countPrimes.toString() + " postMessage(countPrimes(e.data)); };";
const worker = new Worker(URL.createObjectURL(new Blob([source], { type: "text/javascript" })));
document.getElementById("result").textContent = "Working...";
worker.onmessage = function (e) {
document.getElementById("result").textContent = "Worker result: " + e.data;
worker.terminate();
};
worker.postMessage(4000000);
});
</script>
Ticker: 0
Why is a web worker useful?
How do a page and its worker share data?
postMessage, received through the message event. The data is copied, not shared.What does this log?
const source = "postMessage(typeof document);";
const worker = new Worker(URL.createObjectURL(new Blob([source])));
worker.onmessage = e => console.log(e.data);
undefined. A worker has no document, so typeof document is the text "undefined".