Web Workers in JavaScript

JavaScript is single-threaded — it can only do one thing at a time. That means if you run a heavy task (like processing a large list of data, or doing complex calculations), the page freezes until it's done.

A Web Worker is a way to run JavaScript code in the background, on a separate thread. While the worker is busy, your main page stays smooth and responsive.

Real-world analogy: imagine you're at a restaurant. You (the waiter) take orders and serve food. Now imagine the kitchen is doing the cooking — but if you had to cook yourself AND serve, nothing would move. Web Workers are like the kitchen. They handle the heavy work in the back so you can keep serving customers (the UI).

Web Workers were introduced to solve this exact problem — heavy work blocking the UI.

Key things to know:

  • Workers run in a completely separate context from the main page
  • They do NOT have access to the DOM (they can't touch the HTML)
  • They communicate with the main page through messages

Without Web Workers, heavy JavaScript tasks block everything:

javascript

// This will FREEZE the page for a few seconds
function heavyTask() {
  let result = 0;
  for (let i = 0; i < 1_000_000_000; i++) {
    result += i;
  }
  return result;
}

console.log(heavyTask()); // Page is unresponsive while this runs

With a Web Worker, that same task runs in the background — the page stays responsive.

When are Web Workers useful?

  • Processing large datasets (filtering, sorting millions of rows)
  • Complex math or scientific calculations
  • Image processing or video manipulation
  • Parsing large JSON files
  • Running algorithms that take a long time

When are they NOT needed? For most everyday tasks (DOM manipulation, API calls, animations), the Event Loop already handles things efficiently. You only need Web Workers when you have CPU-heavy work that would actually freeze the UI.

A Web Worker lives in a separate JavaScript file. You create it from your main script like this:

javascript

// main.js — your main page script
const worker = new Worker("worker.js");

The worker.js file is the worker's code. It runs completely separately. Here's a simple worker file:

javascript

// worker.js — this runs in the background
self.onmessage = function(event) {
  const number = event.data;

  // Do some heavy work
  let result = 0;
  for (let i = 0; i < number; i++) {
    result += i;
  }

  // Send the result back to the main page
  self.postMessage(result);
};

Inside a worker, self refers to the worker's global scope (like window in the main thread, but without DOM access).

self.onmessage listens for messages sent from the main page.

self.postMessage(result) sends a message back to the main page.

The main page and the worker can only communicate through messages. They do not share memory directly.

  • Main page → Worker: use worker.postMessage(data)
  • Worker → Main page: the worker uses self.postMessage(data)
  • Both sides listen with onmessage

javascript

// main.js

const worker = new Worker("worker.js");

// Listen for messages from the worker
worker.onmessage = function(event) {
  console.log("Worker sent back:", event.data);
  // event.data contains whatever the worker passed to postMessage
};

// Send a message to the worker
worker.postMessage(1_000_000);

console.log("This runs immediately — the main thread is NOT blocked!");

The data is copied (not shared) when passed through messages. This means changes in the worker don't affect the original data in the main thread, and vice versa.

You can also listen for errors:

javascript

worker.onerror = function(error) {
  console.error("Worker error:", error.message);
};

To stop a worker when you're done, call worker.terminate():

javascript

worker.terminate(); // stops the worker immediately

A regular Web Worker is dedicated — it belongs to one page only.

A Shared Worker can be shared across multiple pages, tabs, or iframes that are from the same origin.

Real-world analogy: a regular worker is like a personal assistant hired just for you. A shared worker is like a shared printer that multiple people in the office can use at the same time.

Creating a Shared Worker:

javascript

// main.js (can be used from multiple tabs)
const sharedWorker = new SharedWorker("shared-worker.js");

// Communication happens through a "port"
sharedWorker.port.onmessage = function(event) {
  console.log("Shared worker says:", event.data);
};

sharedWorker.port.postMessage("Hello from tab!");
sharedWorker.port.start(); // required for shared workers

Inside shared-worker.js:

javascript

// shared-worker.js
self.onconnect = function(event) {
  const port = event.ports[0];

  port.onmessage = function(msg) {
    console.log("Got:", msg.data);
    port.postMessage("Reply from shared worker");
  };
};

Shared Workers are less commonly used but useful when multiple tabs need to share state or coordinate tasks.

Web Workers are powerful, but they come with important restrictions:

  • No DOM access — Workers cannot read or modify HTML elements. They have no access to document, window, or any DOM API.
  • No window object — The global object is self, not window. alert(), confirm(), localStorage are not available.
  • Same-origin policy — The worker script file must be on the same origin as the page. You cannot load a worker from another domain.
  • File protocol limitation — Web Workers do not work when you open an HTML file directly from your computer (using file://). You need a server.
  • Communication overhead — Data passed through messages is copied, not shared. For very large data, this copying can become slow. (There is a workaround using Transferable Objects, but that is an advanced topic.)

Despite these limits, Web Workers are extremely valuable for any CPU-intensive work that would freeze the UI.

Example of what NOT to do in a worker:

javascript

// worker.js — this will FAIL

// document.getElementById("output").textContent = "Done!"; // Error!
// window.alert("Finished!"); // Error!
// localStorage.setItem("key", "value"); // Error!

// What you CAN do:
self.postMessage("Done!"); // send a message to the main thread instead
  • JavaScript is single-threaded — without Web Workers, heavy tasks freeze the entire page.
  • A Web Worker runs JavaScript in a background thread, keeping the main thread free for the UI.
  • Create a worker with new Worker("worker.js") — the worker code lives in a separate file.
  • Workers communicate with the main page through messages: postMessage() to send, onmessage to receive.
  • Data is copied (not shared) when passed through messages. Workers do not share memory with the main thread.
  • Stop a worker with worker.terminate().
  • Workers have no DOM access — they cannot read or change HTML. They use self instead of window.
  • Shared Workers can be used by multiple tabs/pages from the same origin, useful for coordinating shared state.
  • Use Web Workers for CPU-heavy tasks: processing large data, complex calculations, image/video processing.
  • For most everyday tasks (DOM updates, API calls), the Event Loop is sufficient — you do not need Web Workers.
What's next? Head back to Introduction to review the basics, or explore other advanced topics in the sidebar.
  • What is a Web Worker in JavaScript?
  • Why do we need Web Workers? What problem do they solve?
  • How do you create a Web Worker?
  • How does the main thread communicate with a Web Worker?
  • What is self inside a Web Worker?
  • What does postMessage() do, and how is the data transferred?
  • Can a Web Worker access the DOM? Why or why not?
  • How do you stop a Web Worker?
  • What is the difference between a Dedicated Worker and a Shared Worker?
  • Name three types of tasks that are good candidates for Web Workers.
  • What happens if a Web Worker throws an uncaught error?
  • What is the onerror handler on a Worker used for?