Complete HTML Course Module 14: Advanced: HTML Web APIs

HTML Web Workers API: Run Scripts in the Background

  • Beginner
  • Lesson 14.5
  • 6 min read
  • 62 of 69 in the course

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);
};
  • self means the worker itself (inside a worker there is no window).
  • self.onmessage runs when the page sends a message. The message is in event.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.onmessage runs when the worker sends something back; event.data is the count.
  • worker.onerror shows 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 in htdocs and open them through http://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:

MethodCalled fromWhat it does
worker.terminate()The pageStops the worker at once, even in the middle of a task
self.close()Inside the workerThe 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 useWorkers CANNOT use
fetch() to load dataThe DOM: document, elements, querySelector
setTimeout and setIntervalThe window object
IndexedDB (browser database)localStorage and sessionStorage
WebSocketsalert(), confirm(), prompt()
importScripts() or modules to load other codeVariables 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 use import inside 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() and onmessage; the data is in event.data and is copied.
  • Stop a worker with worker.terminate() from the page or self.close() inside it.
  • Workers cannot touch the DOM, window or localStorage, and must be loaded through a server, not file://.