Web Workers in JavaScript

Why should you care about Web Workers in JavaScript?

Web Workers let you move heavy tasks off the main thread so your app stays responsive. This is a practical skill for handling large data, smooth UI interactions, and performance focused interview questions.

Advanced topic: Learn functions, events, and asynchronous code first. A worker is useful when JavaScript performs enough CPU-heavy work to freeze the page; it is not a replacement for fetch, async/await, or ordinary DOM event handlers.

JavaScript on the main thread runs one task at a time. If a task takes too long, it can block the page and make it feel unresponsive.

A Web Worker lets JavaScript run on another thread. CPU-heavy work can run there while the main thread keeps handling the page.

A worker has its own JavaScript context and event loop. It cannot access the page's DOM, so it communicates with the main page by sending messages.

Key things to know:

  • Workers have their own JavaScript execution context and event loop
  • 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. async/await helps you manage asynchronous operations, but it does not move JavaScript execution to another thread.

Counting prime numbers up to a large limit is CPU-heavy. If the main page performs the loop itself, buttons and rendering must wait. The worker performs the loop and sends progress and the final answer back with postMessage.

worker.js: keep computation in the worker. It cannot update the page directly, so it sends small messages instead.

worker.js

function isPrime(number) {
  if (number < 2) return false;
  for (let divisor = 2; divisor * divisor <= number; divisor++) {
    if (number % divisor === 0) return false;
  }
  return true;
}

self.onmessage = ({ data }) => {
  const limit = data.limit;
  let primeCount = 0;

  for (let number = 2; number <= limit; number++) {
    if (isPrime(number)) primeCount++;

    if (number % 100_000 === 0) {
      self.postMessage({ type: "progress", value: number / limit });
    }
  }

  self.postMessage({ type: "done", primeCount });
};

main.js: create the worker, show progress in the UI, and stop it when the work is no longer needed.

main.js

const worker = new Worker("worker.js");
const status = document.querySelector("#status");

worker.onmessage = ({ data }) => {
  if (data.type === "progress") {
    status.textContent = `Progress: ${Math.round(data.value * 100)}%`;
  }

  if (data.type === "done") {
    status.textContent = `Found ${data.primeCount} prime numbers.`;
    worker.terminate();
  }
};

worker.onerror = (error) => {
  status.textContent = `Worker failed: ${error.message}`;
};

status.textContent = "Working... the page can still respond.";
worker.postMessage({ limit: 5_000_000 });

// A Cancel button could call this before the work finishes:
// worker.terminate();

The main thread remains available for clicks and painting because the prime-number loop runs in the worker. In a real page, #status would be an element such as <p id="status"></p>. Try a smaller limit first, then increase it while watching the progress updates.

Important: this example must run from a local development server, not by opening the HTML file with file://. The browser needs to load worker.js as a separate script.

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");
const moduleWorker = new Worker("worker-module.js", { type: "module" });

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 is the worker's global object. Workers do not have the page's window or DOM.

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 a worker normally communicate through messages. Ordinary messages copy data instead of sharing the same object in memory.

  • 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!");

Normal messages use the structured clone algorithm. The browser copies supported data, so changes in the worker do not change the original object on the main thread. For some large binary values, Transferable objects can transfer ownership instead of copying the data.

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 worker can also stop itself from inside worker.js by calling self.close().

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

Shared Workers are an advanced topic. Most applications can start with Dedicated Workers. A Shared Worker can be used by multiple pages, tabs, or iframes from the same origin.

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:

Decide whether a task is a good fit for a Web Worker.
Task Worker fit Reason
Count primes or process a large dataset Good fit CPU-heavy work can run without blocking the main thread.
Change a button or read an input Not a fit Workers cannot access the DOM; keep UI work on the main thread.
Fetch one small API response Usually not needed fetch and async/await already handle waiting without blocking the page.
Send a very large object repeatedly Use caution Structured cloning copies messages and the transfer cost may remove the benefit.
  • No DOM access - Workers cannot read or modify HTML elements. They cannot use page DOM APIs like document.
  • No window object - The global object is self, not window. So alert(), confirm(), and localStorage are not available, but many worker-safe APIs (like fetch and timers) are available.
  • Origin and security rules - Worker scripts follow origin and CORS security rules. In common setups, you load them from the same origin. Cross-origin loading needs explicit server permission.
  • 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 normal messages is usually copied. For very large data, copying can become slow. Transferable objects can move ownership instead.

Despite these limits, Web Workers are useful when CPU-heavy work would make the page unresponsive.

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

Use a Web Worker when:

  • JavaScript performs CPU-heavy calculations.
  • The work takes long enough to make the page unresponsive.
  • The work does not need direct DOM access.
  • The work can communicate with the page through messages.

You probably do not need one when:

  • You are making a normal API request.
  • You are changing a few DOM elements.
  • You are handling normal user events.
  • The task is small and finishes quickly.

Remember: async/await helps manage asynchronous operations, but it does not move CPU-heavy JavaScript to another thread.

Advanced: Shared Memory

JavaScript also provides SharedArrayBuffer and Atomics for controlled shared memory between threads. These APIs usually require cross-origin isolation and are advanced topics. They are not required to start using Web Workers.

  • Web Workers run JavaScript on another thread.
  • They are useful for CPU-heavy work that could block the main thread.
  • Workers cannot access the DOM. They communicate with the page through postMessage() and onmessage.
  • async/await manages asynchronous operations; it does not replace a worker for CPU-heavy work.
  • Stop a worker with worker.terminate(), or from inside the worker with self.close().
  • Shared Workers are an advanced option for multiple pages or tabs from the same origin.
What's next? Head back to Introduction to review the basics, or explore other advanced topics in the sidebar.

Reviewed by

SimplyJavaScript Editorial Team

Technical editors and JavaScript educators with hands-on experience building frontend projects, writing learning material, and reviewing tutorials for clarity, accuracy, and beginner-friendly guidance.