SKU // 137

JS Web Worker

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.

// The Problem: One Thread

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.

// How a Worker Works

A 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.

// A Worker You Can Run Here

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);
42

Notice the order: worker.postMessage(21) returns at once, and the answer arrives later in onmessage. Like timers and fetch, a worker is asynchronous.

// Real Work: Counting Primes

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);
9592 primes below 100000

// Messages Are Copies

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);
3 4 false

// What a Worker Cannot Do

const 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();
};
["undefined", "undefined", "object", "function"]

// Errors and Stopping

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();
};
worker failed: true

// A Page That Stays Responsive

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>

// When to Use One

Other kinds of workers
This lesson covers the dedicated worker. A shared worker can serve several tabs at once, and a service worker sits between the page and the network and makes sites work offline. They use the same message idea.

// Quick Check

Why is a web worker useful?

  1. It makes the user's internet faster
  2. It runs heavy work on its own thread so the page stays responsive
  3. It lets code change the page from the background
  4. It stores data between visits
Show answer
B. A worker runs separately, so a long calculation does not freeze the page. It cannot touch the page directly.

How do a page and its worker share data?

Show answer
By passing messages with 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);
Show answer
undefined. A worker has no document, so typeof document is the text "undefined".

// Try It

  1. Click Open Playground and run it. Change the limit and see how the time changes
  2. Change the worker so it sends back the sum of the numbers from 1 to the limit
  3. Send an object with a name and a list, and have the worker send back the list length
  4. Press both buttons in the responsive demo and describe in your notes what happens to the ticker
// Video Walkthroughs
Last updated: Oct 11, 2026, 9:11 PM ET