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 or document.
  • 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.


_10
const 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:


_10
self.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 and TypedArray.

Examples of unsupported types are:

  • Functions.
  • DOM nodes (e.g., document or Element).

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.


_10
worker.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.


_10
const worker = new Worker('dedicated-worker.js');
_10
_10
worker.onmessage = (event) => {
_10
console.log('Received:', event.data);
_10
};
_10
_10
worker.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:


_10
self.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:


_10
const worker = new SharedWorker('shared-worker.js');
_10
_10
worker.port.onmessage = (event) => {
_10
console.log('Message from shared worker:', event.data);
_10
};
_10
_10
worker.port.start();
_10
worker.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.


_10
if ('serviceWorker' in navigator) {
_10
navigator.serviceWorker.register('/service-worker.js').then(() => {
_10
console.log('Service Worker registered successfully!');
_10
});
_10
}


_11
self.addEventListener('install', (event) => {
_11
console.log('Service Worker installed');
_11
});
_11
_11
self.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.


_10
const worker = new Worker('module-worker.js', { type: 'module' });
_10
_10
worker.onmessage = (event) => {
_10
console.log('Message from module worker:', event.data);
_10
};
_10
_10
worker.postMessage('Hello, Module Worker!');


_10
import { heavyTask } from './utils.js';
_10
_10
self.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.


_10
CSS.paintWorklet.addModule('paint-worklet.js');

in the worklet we can then define our paint function:


_11
registerPaint('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:


_10
div {
_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.


_10
self.onmessage = function (event) {
_10
const number = event.data;
_10
const result = factorial(number);
_10
self.postMessage(result);
_10
};
_10
_10
function 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.


_11
if (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.

Stay ahead of the pack 🐶

Join the newsletter to get the latest articles and expert advice directly to your inbox.
Totally free, no spam and you can unsubscribe anytime you want!

  • Expert Tips
  • No Spam
  • Latest Updates

I'll never share any of your information with a third party. That's a promise.

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.

  1. Install Comlink:


    _10
    npm install comlink

  2. Refactor your worker and main thread:


    _10
    import * as Comlink from 'comlink';
    _10
    _10
    function factorial(n) {
    _10
    if (n === 0 || n === 1) return 1;
    _10
    return n * factorial(n - 1);
    _10
    }
    _10
    _10
    Comlink.expose({ factorial });


    _11
    import * as Comlink from 'comlink';
    _11
    _11
    async function main() {
    _11
    const worker = new Worker('worker.js');
    _11
    const api = Comlink.wrap(worker);
    _11
    _11
    const result = await api.factorial(10);
    _11
    console.log('Factorial result:', result);
    _11
    }
    _11
    _11
    main();

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.

1) Which type of worker allows multiple browser tabs to share a single thread?

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?

D is for danny

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.

Related Articles

Gobacktothetop

Made with 🐾 in 🏴󠁧󠁢󠁥󠁮󠁧󠁿

©2024 All rights reserved.