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.
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:
// 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.
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.
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.
A Web Worker lives in a separate JavaScript file. You create it from your main script like this:
// 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:
// 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
// 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:
worker.onerror = function(error) {
console.error("Worker error:", error.message);
};
To stop a worker when you're done, call worker.terminate():
worker.terminate(); // stops the worker immediately
A worker can also stop itself from inside worker.js by calling self.close().
Web Workers are powerful, but they come with important restrictions:
| 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:
// 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 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?
- What is the difference between worker.postMessage() and self.postMessage()?
- What is the structured clone algorithm?
- What are Transferable objects, and when can they help?
- 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?
- What types of CPU-heavy tasks are good candidates for a Web Worker?