Link copied!
Optimizing JavaScript Performance with Web Workers: A Deep Dive into Dedicated and Shared Workers Technical Log

TechiesAIE Journal

Optimizing JavaScript Performance with Web Workers: A Deep Dive into Dedicated and Shared Workers

TechiesAIE
TechiesAIE
Lead Developer · TechiesAIE
4 min read 852 words

Based on the sources linked below.

Cover image: Aboutbigdata · CC BY-SA 4.0 · License · Image source

JavaScript applications typically run on a single thread, which means that computationally intensive operations can block the user interface, leading to a sluggish and unresponsive experience. Web Workers offer a solution by enabling scripts to run in background threads, effectively introducing multithreading capabilities to the browser environment. This allows demanding tasks to execute without freezing the main thread, significantly enhancing application performance and user experience.

Web Workers operate in a global context separate from the main window, meaning they cannot directly manipulate the Document Object Model (DOM). However, they can perform network requests using Fetch or XMLHttpRequest APIs and access data storage mechanisms like IndexedDB. Communication between the main thread and a worker occurs through a messaging system, where data is copied—rather than shared—between contexts.

Dedicated Workers: Single-Script Background Tasks

A dedicated worker is the most common type, accessible only by the script that created it. This makes them suitable for isolated, heavy computations that don't need to be shared across multiple parts of an application. To create a dedicated worker, you instantiate a `Worker()` object, providing the URI of the script that will run in the background thread.

For example, to offload a multiplication task, you would create a worker like this: `const myWorker = new Worker("worker.js");`. Modern bundlers recommend resolving the worker script's URL relative to `import.meta.url` for safe optimizations, like `new Worker(new URL("worker.js", import.meta.url));`.

Communication with Dedicated Workers

The interaction between the main thread and a dedicated worker relies on the `postMessage()` method for sending data and the `onmessage` event handler for receiving data. When the main script wants to send data to the worker, it calls `myWorker.postMessage(data)`. The worker then receives this data via its `onmessage` handler, processes it, and sends a response back to the main thread using `postMessage(result)`.

For instance, if the main thread sends two numbers, `[first.value, second.value]`, to `worker.js`, the worker's `onmessage` handler would process them: `onmessage = (e) => { const workerResult = e.data[0] * e.data[1]; postMessage(workerResult); };`. The main thread then handles the response with its own `myWorker.onmessage = (e) => { result.textContent = e.data; };`.

Data passed between threads is copied using a structured cloning algorithm, which supports various JavaScript objects, including circular references. For immediate termination, the main thread can call `myWorker.terminate()`, which abruptly stops the worker's execution.

Shared Workers: Multipage Multithreading

Unlike dedicated workers, a shared worker can be accessed by multiple scripts, even from different windows, iframes, or other workers, provided they share the exact same origin (protocol, host, and port). This makes shared workers ideal for centralized background tasks or managing a single, consistent state across various parts of a web application.

To create a shared worker, you use the `SharedWorker()` constructor: `const myWorker = new SharedWorker("worker.js");`. A key difference in shared worker communication is the necessity of a `port` object, which acts as an explicit channel for messages. The connection to this port needs to be initiated, either implicitly by setting up an `onmessage` event handler on the port or explicitly with the `start()` method.

Communication with Shared Workers

When a script connects to a shared worker, the worker's `onconnect` event handler is triggered. This handler receives an event object containing the `ports` attribute, which is an array of `MessagePort` objects. The worker uses `e.ports[0]` to get the specific port for communication and then attaches an `onmessage` handler to it to receive data: `port.onmessage = (e) => { const workerResult = e.data[0] * e.data[1]; port.postMessage(workerResult); };`.

Messages from the main thread are sent via `myWorker.port.postMessage(data)`, and responses are received through `myWorker.port.onmessage = (e) => { result.textContent = e.data; };`. Shared workers remain alive as long as they are referenced by at least one browsing context. They can also be kept alive for a short period after all references close using the `extendedLifetime: true` option during construction, allowing for tasks like state persistence or analytics transmission after a user navigates away.

Error Handling and Script Imports

Web Workers provide an `onerror` event handler for runtime errors, which receives an `ErrorEvent` object containing details like `message`, `filename`, and `lineno`. This allows for robust error management in background threads.

Workers can also import external scripts and libraries using the `importScripts()` global function. This function accepts one or more URIs, loading and executing scripts synchronously. Any global objects from the imported scripts become available within the worker's scope, facilitating modular code organization and reuse.

Practical Uses and Limitations

Web Workers are invaluable for tasks that would otherwise block the main thread, such as complex calculations, data processing, image manipulation, or fetching large datasets. By moving these operations to a background thread, the user interface remains responsive, providing a smoother experience.

However, workers cannot directly access the DOM or certain `window` object properties. Data transfer between the main thread and workers involves copying, which can introduce overhead for very large datasets, though transferable objects can mitigate this by transferring ownership with zero-copy operations for specific data types. Despite these limitations, Web Workers provide a powerful mechanism for enhancing the performance of web applications by leveraging multithreading.

Sources