What are Web Workers and when should you use them?
Introduction
JavaScript in the browser normally runs on a single main thread that handles user input, renders the page and executes your code. If your code performs a heavy computation, the browser cannot respond to user input until that computation finishes, leading to a frozen interface. Web workers provide a simple way to run scripts in background threads so long-running tasks don't block the UI.
What is a web worker?
A web worker is created by calling new Worker('worker.js'), which spawns a new thread running the specified script. Workers have their own global context (similar to a separate window), cannot directly access the DOM and communicate with the main thread via message passing. Workers run scripts in background threads and can perform tasks without interfering with the user interface. Once created, a worker can send messages to the main thread and receive messages back using postMessage() and the onmessage event handler.
Basic Structure
Main thread (page)
// main.js
const worker = new Worker("worker.js");
worker.onmessage = (e) => {
console.log("Received from worker:", e.data);
};
worker.postMessage({ type: "start", value: 40 });
// later, terminate the worker
// worker.terminate();
Worker script
// worker.js
self.onmessage = (e) => {
const { type, value } = e.data;
if (type === "start") {
const result = fib(value);
self.postMessage(result);
}
};
function fib(n) {
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}
Let's walk through the interaction step by step to see what each line does and when each handler is invoked.
1. Creating the worker
const worker = new Worker("worker.js");
On the main thread (the page), this line spawns a new background thread and instructs it to run the code contained in worker.js. A worker has its own global scope separate from the main thread and cannot access the DOM or use most of the window object. The Worker constructor returns a Worker instance that the main thread can use to communicate with this background thread.
2. Setting up a message handler on the main thread
worker.onmessage = (e) => {
console.log("Received from worker:", e.data);
};
The onmessage property of the Worker instance is an event handler for the "message" event. It is called whenever the worker thread sends a message back to the main thread. The handler receives an event object (e), whose data property contains whatever data the worker posted. At this point, nothing is called yet; you're just registering a callback so you can handle responses in the future.
3. Posting a message to the worker
worker.postMessage({ type: "start", value: 40 });
Here, the main thread sends a message to the worker. It uses postMessage(), which serializes the given data and delivers it to the worker thread. Because workers communicate via message passing, data is copied rather than shared. In this example, the main thread sends an object with a type of "start" and a value of 40, instructing the worker to begin a computation.
4. Handling the message in the worker
In worker.js, the worker registers its own onmessage handler:
self.onmessage = (e) => {
const { type, value } = e.data;
if (type === "start") {
const result = fib(value);
self.postMessage(result);
}
};
The self keyword inside the worker refers to the worker's global scope. When the main thread calls postMessage(), the worker's onmessage handler fires, receiving the data in e.data. The worker checks the message type; if it is "start", it calls the fib() function to compute the 40th Fibonacci number. After finishing, it calls self.postMessage(result) to send the result back to the main thread.
5. Completing the round trip
Once the worker posts the result, the browser delivers it to the main thread and triggers the handler you assigned earlier:
worker.onmessage = (e) => {
console.log("Received from worker:", e.data);
};
At this point, e.data contains the Fibonacci number computed in the worker. The callback logs the value. Any subsequent messages from the worker will also trigger this handler.
6. Cleaning up
If you no longer need the worker, you can terminate it:
// worker.terminate();
Calling terminate() immediately stops the worker's thread and frees its resources. Putting it all together: the main thread creates a worker, sets up a response handler (onmessage), and sends a message using postMessage(). The worker receives that message in its own onmessage handler, performs the computation, and sends the result back using postMessage(). The main thread then receives the result via its onmessage callback. Because the heavy computation runs in a separate thread, the user interface remains responsive throughout.
Types of workers
- Dedicated workers: The most common type. A dedicated worker is tied to a single script. Only the thread that created it can communicate with it.
- Shared workers: A shared worker can be accessed from multiple scripts running in different windows or tabs, provided they are from the same origin.
- Service workers: A special worker that intercepts network requests, enabling offline caching and background sync. Service workers are not directly used for computational tasks, but they share some worker characteristics.
When to use web workers
Use web workers when you need to perform CPU-intensive or blocking operations that would otherwise freeze the UI. Examples include:
- Data processing: Sorting large arrays, parsing big JSON files, doing cryptographic operations or image manipulation.
- Network requests: Although fetch runs asynchronously, combining requests with heavy processing (like decompressing large files) benefits from a worker.
- Real-time calculations: Physics simulations or game logic. Avoid using a worker for simple tasks or frequent updates that would incur unnecessary message passing overhead. Also remember that workers cannot directly access DOM elements or most of the
windowobject.
Real-world analogy
Think of a web worker as a personal assistant. While you (the main thread) interact with users and handle immediate tasks, your assistant can work on a complex report in another room. You occasionally exchange notes (messages), but you don't look over each other's shoulders. If you want the report to stop, you tell your assistant to stop working (terminate the worker).
Common misconceptions
- "Web workers can access the DOM." They cannot. Only the main thread can manipulate the DOM.
- "Workers share memory with the main thread." They communicate by copying or transferring data via messages; data is not shared.
- "Workers always improve performance." Spawning a worker has overhead. For small tasks, it's cheaper to run them on the main thread.
Practice questions
- What problem do web workers solve?
- How do the main thread and a worker communicate?
- Why can't a worker access the DOM?
- When might using a worker be unnecessary or counterproductive?