What Are Web Workers?
At their core, Web Workers are a way to run JavaScript code in a separate thread. This means tasks running in a Web Worker won’t block the main thread, which is responsible for rendering your UI and handling user interactions.
Here’s the key idea:
Main thread = User Interface.
Worker thread = Heavy Computational Lifting.
When you have computationally heavy tasks, such as data parsing, image processing, or complex mathematical calculations, Web Workers step in to handle the load.
Why Use Web Workers?
Imagine you're building a photo editor. When a user applies a filter to an image, the processing can take a while. Without a Web Worker, the UI might freeze, making the app feel unresponsive. By offloading the computation to a Web Worker, the main thread remains free to handle user interactions.
Web Workers run in a separate thread, leveraging multicore CPUs effectively. By distributing the workload, you can achieve significant performance gains, especially in resource-intensive applications.
A fast, responsive UI gives users a good experience. By utilising Web Workers, you ensure the user has the best experience possible.
The Limitations of Web Workers
There’s a few key things that web workers aren’t that good at dealing with. It might be worth assesing these limitations before deciding if they are suitable for your use case.
They can’t access the DOM - Workers cannot manipulate the DOM directly. This limitation enforces thread safety but restricts certain use cases.
Serialisation Overhead - Message-passing involves copying data, which can become a bottleneck for large objects.
Resource Limits - The number of workers is limited by browser policies and system resources.
Browser Support - While Web Workers are supported in modern browsers, older browsers may lack full compatibility, you can check what browsers currently support Web Workers here.
The Architecture of Web Workers
Web Workers are an implementation of background threads in the browser environment. They allow JavaScript code to run independently of the main thread, which handles the DOM, rendering, and user interactions.
Each instance of a Web Worker runs in its own isolated thread. This thread holds a specific global context for the worker referred to as self
instead of window
.
Because it lives in it’s own thread, each worker has its own event loop, which is independent of the main thread. This loop handles tasks such as executing the worker script, processing messages sent via postMessage
and handling asynchronous tasks like fetch
.
They Integrate With the Browser
The browser kernel is responsible for spawning the worker thread and managing the worker’s memory and execution lifecycle it also deals with facilitating inter-thread communication, this means we have access to to core browser APIs like fetch
, XMLHttpRequest
, setTimeout
, and WebAssembly in our Web Worker code.
The browser manages a pool of threads for Web Workers, when a worker is created; a new thread is allocated from the pool if the system resources permit it. If the pool is full, workers may be queued or fail to initialise.
This thread pooling ensures efficient use of system resources.
They’re Sandboxed
Web Workers operate in a secure sandbox. They cannot:
- Access the DOM directly.
- Interact with
window
ordocument
. - Access local storage or cookies.
This isolation prevents security risks, such as race conditions or thread conflicts.
Lifecycle of a Web Worker
The lifecycle of a Web Worker involves creation, execution, and termination. Let’s break this down step by step.
Creation
A Web Worker is created in the main thread using the Worker
constructor.
_10const worker = new Worker('worker.js');
When instantiated, the browser spawns a new thread or reuses an existing thread from its pool. The worker script worker.js
is then loaded and executed in its own context.
Execution
After the Web Worker is initialised; it listens for messages from the main thread using onmessage
. Your worker.js
would look something like this:
_10self.onmessage = (event) => {_10 console.log('Message received in worker:', event.data);_10};
Workers execute tasks independently, ensuring that heavy computations or asynchronous operations do not block the main thread.
The Communication Model
Communication between the worker thread and main thread happen by utilising message-passing model.
postMessage(data)
to send data from the main thread.
onmessage
to receive data in the worker thread.
This communication relies on the structured clone algorithm. This ensures that the worker operates on a separate copy, maintaining thread safety. This serialises data passed between threads. This avoids issues with shared memory but in turn can be slower for large data transfers.
There are several supported data structure types that we can use to pass data between the main thread and the Web Worker thread. These are called Transferable Objects which include:
- Primitive types: Numbers, strings, booleans.
- Objects and arrays.
- Binary data:
ArrayBuffer
andTypedArray
.
Examples of unsupported types are:
- Functions.
- DOM nodes (e.g.,
document
orElement
).
Termination
Web Workers can be terminated explicitly or allowed to run to completion:
Explicit Termination happens when the main thread terminates a worker using worker.terminate()
. This stops the worker immediately, discarding any unfinished tasks.
_10worker.terminate();
Implicit Termination is when the worker terminates when the script completes or the browser unloads the page.
Types Of Workers
While Dedicated Workers are the most commonly used, advanced types like Shared Workers, Service Workers, and emerging concepts like Module Workers and Worklets offer powerful capabilities that extend the worker ecosystem.
Dedicated Workers
Dedicated Workers are tied to a single script or page.
They’re used for general-purpose background tasks like data processing, heavy computations, or rendering offloaded tasks. The worker runs in its own thread and communication is strictly between the main thread and the worker.
_10const worker = new Worker('dedicated-worker.js');_10_10worker.onmessage = (event) => {_10 console.log('Received:', event.data);_10};_10_10worker.postMessage('Hello from the main thread!');
Shared Workers
Shared Workers as the name suggests are shared across multiple scripts, tabs, windows, or iframes from the same origin. This helps when coordinating state or tasks across multiple tabs, such as a messaging system or shared resource management. Unlike Dedicated Workers, Shared Workers can serve multiple clients such as multiple browser tabs.
This makes them deal for managing a shared cache or a shared WebSocket connection.
One thread handles multiple clients, reducing resource usage compared to spawning multiple Dedicated Workers. This is achieve by communicating via MessagePort
objects. For example the worker script looks like this:
_10self.onconnect = (event) => {_10 const port = event.ports[0];_10_10 port.onmessage = (msg) => {_10 console.log('Message received in shared worker:', msg.data);_10 port.postMessage(`Hello, ${msg.data}!`);_10 };_10};
The main thread then allows you to communicate via the port on like so:
_10const worker = new SharedWorker('shared-worker.js');_10_10worker.port.onmessage = (event) => {_10 console.log('Message from shared worker:', event.data);_10};_10_10worker.port.start();_10worker.port.postMessage('Main thread');
But don’t get too excited, Shared Workers aren’t currently widely supported.
Service Workers
A Shared Worker acts as a proxy between the browser and the network, enabling background tasks like caching, push notifications, and offline support.
They act as a key component in Progressive Web Apps for caching assets and responding to network requests locally, as well as queuing and synchronise tasks when the device comes online, they can also allow you to send updates even when the app is closed.
_10if ('serviceWorker' in navigator) {_10 navigator.serviceWorker.register('/service-worker.js').then(() => {_10 console.log('Service Worker registered successfully!');_10 });_10}
_11self.addEventListener('install', (event) => {_11 console.log('Service Worker installed');_11});_11_11self.addEventListener('fetch', (event) => {_11 event.respondWith(_11 caches.match(event.request).then((response) => {_11 return response || fetch(event.request);_11 })_11 );_11});
Module Workers
Module Workers are really similar to Dedicated Workers but with support for ES module imports and syntax. This enables you to use modular JavaScript applications with clean dependency management by simplifying imports which enhances code organisation. Module Workers also offer better performance due to tree-shaking and caching optimisations.
_10const worker = new Worker('module-worker.js', { type: 'module' });_10_10worker.onmessage = (event) => {_10 console.log('Message from module worker:', event.data);_10};_10_10worker.postMessage('Hello, Module Worker!');
_10import { heavyTask } from './utils.js';_10_10self.onmessage = (event) => {_10 const result = heavyTask(event.data);_10 self.postMessage(result);_10};
Worklets
Finally, Worklets are lightweight workers designed for highly specialised tasks such as utilising CSS Paint API, Audio Worklet API, and Animation Worklet API.
They are best suited for tasks that require frequent and rapid execution. Althought they tun in a different thread from the main thread they share the same JavaScript context.
_10CSS.paintWorklet.addModule('paint-worklet.js');
in the worklet we can then define our paint function:
_11registerPaint('my-paint', class {_11 static get inputProperties() {_11 return ['--background-color'];_11 }_11_11 paint(ctx, size, properties) {_11 const color = properties.get('--background-color').toString();_11 ctx.fillStyle = color || 'black';_11 ctx.fillRect(0, 0, size.width, size.height);_11 }_11});
and finally in our CSS file we can include it:
_10div {_10 background: paint(my-paint);_10 --background-color: red;_10}
Setting Up Your First Web Worker
Let’s jump into a simple example. We'll create a Web Worker to calculate the factorial of a number—an operation that can be computationally expensive for large inputs.
Step 1 - Create the Web Worker File
Create a new file called worker.js
.
_10self.onmessage = function (event) {_10 const number = event.data;_10 const result = factorial(number);_10 self.postMessage(result);_10};_10_10function factorial(n) {_10 if (n === 0 || n === 1) return 1;_10 return n * factorial(n - 1);_10}
In this example self.onmessage
listens for messages sent to the worker. When a message is received, the worker calculates the factorial and the result is sent back to the main thread using self.postMessage
.
Step 2 - Set Up the Main Thread
In your main JavaScript file, integrate the worker.
_11if (window.Worker) {_11 const worker = new Worker('worker.js');_11_11 worker.onmessage = function (event) {_11 console.log('Factorial result:', event.data);_11 };_11_11 worker.postMessage(10);_11} else {_11 console.error('Web Workers are not supported in this browser.');_11}
In the main thread, a new Web Worker is instantiated using the Worker
constructor, the worker.postMessage(10)
sends the number 10
to the worker.
When the worker completes the computation, it sends the result back, which is logged to the console.
Advanced Use Cases for Web Workers
Data Processing
Handling large datasets in the browser can be taxing. Web Workers are perfect for processing or filtering data without blocking the UI. For example this can be parsing a large JSON file or performing real-time data analytics.
Image Manipulation
Web Workers shine in image-heavy applications, like editors or visualisers. Using libraries like fabric.js
or pixi.js
, you can offload intensive tasks, such as resizing, filtering, or compression.
Web Assembly & Machine Learning
Web Workers complement WebAssembly and machine learning libraries by offloading computation-heavy tasks.
Running TensorFlow.js
models in a worker to keep your UI responsive.
Simplifying Communication
There are libraries that exist that help implementing Web Workers much easier. One such example is Comlink. Here’s how that would work if we were to refactor our factorial Web Worker example.
-
Install Comlink:
_10npm install comlink -
Refactor your worker and main thread:
_10import * as Comlink from 'comlink';_10_10function factorial(n) {_10if (n === 0 || n === 1) return 1;_10return n * factorial(n - 1);_10}_10_10Comlink.expose({ factorial });_11import * as Comlink from 'comlink';_11_11async function main() {_11const worker = new Worker('worker.js');_11const api = Comlink.wrap(worker);_11_11const result = await api.factorial(10);_11console.log('Factorial result:', result);_11}_11_11main();
Using Web Workers Effectively
Keep Tasks Modular
Web Workers should perform specific, self-contained tasks. This makes them easier to manage and debug.
Minimize Data Transfer
Passing large amounts of data between the main thread and a worker can become a bottleneck. Use transferable objects like ArrayBuffer
for efficient communication.
Error Handling
Always include error handling for both the main thread and workers. Use worker.onerror
to catch issues in your worker code.
Conclusion
Web Workers are a game-changer for frontend engineers looking to build fast and responsive applications. By offloading heavy computations to a separate thread, they keep the main thread free to handle what matters most—your users. Whether you're processing data, manipulating images, or running machine learning models, Web Workers provide a robust way to optimise performance.
Start experimenting with Web Workers in your projects and watch your app’s performance soar! And if you’re hungry for more performance tips, check out our articles on Understanding Browser Rendering.
Reinforce Your Learning
One of the best ways to reinforce what your learning is to test yourself to solidify the knowlege in your memory.
Complete this 6 question quiz to see how much you remember.
Thanks alot for your feedback!
The insights you share really help me with improving the quality of the content here.
If there's anything you would like to add, please send a message to:
[email protected]Was this article this helpful?
About the author
Danny Engineering
A software engineer with a strong belief in human-centric design and driven by a deep empathy for users. Combining the latest technology with human values to build a better, more connected world.