Harnessing Parallelism: A Guide to JavaScript Web Workers

Murali SinghMurali Singh
8 min read

Web Workers are a simple means for web content to run scripts in background threads.

If you're wondering how JavaScript, known for its single-threaded nature, manages to execute scripts in background threads, let's dive into how web workers make this possible.

The Web Workers functionality in JavaScript is provided through an API called the Web Workers API. This API allows web developers to create separate JavaScript files (worker scripts) that run in their own thread. This means that while the main thread (where your regular JavaScript code runs) continues to handle user interactions and updates the UI, the web worker can execute scripts concurrently in the background.

  1. Types of Web Workers

Web workers in JavaScript come in two main types:

  • Dedicated Workers: These are tied to a specific script file and are used for tasks that require a single background thread.

  • Shared Workers: These can be accessed by multiple scripts or pages within the same origin (domain) and are useful for applications that require communication between different tabs or windows.

  1. How Communication Works ?

  • In a shared worker, the connection is initiated using port.start() from the created instance.

  • Sending Messages: You send messages between the main thread and the worker using the postMessage method.

  • Receiving Messages: Both the main thread and the worker listen for messages using the onmessage event handler.

  • Terminate:

    A dedicated worker communication can be terminated using the terminate() method from the main thread or another context that created it. This method is accessible on the Worker object and allows the controlling thread to stop the worker's execution and release its resources.

    In shared worker, termination involves closing the connections (ports) from all contexts that have connected to it. The close() method on the SharedWorkerGlobalScope interface allows controlling contexts (like browser tabs or windows) to close their connection to the shared worker, which triggers the onclose event on the shared worker's ports, signaling cleanup and resource release.

  • Error Handling:

    When a runtime error occurs in the worker, its onerror event handler is called. It receives an event named error which implements the ErrorEvent interface.

Understanding these concepts may seem overwhelming, and you might be feeling a bit sleepy after absorbing all this information. However, in the next section, we'll dive into practical code examples. I hope you find them enjoyable!

  1. Dedicated Worker Example

HTML : This file sets up the basic structure of the web page with an input field for the limit, a button to start the worker, and a div to display the results.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Prime Number Calculator</title>
</head>
<body>
    <input type="number" id="limit" placeholder="Enter a limit">
    <button id="startWorker">Calculate Primes</button>
    <div id="result"></div>
    <script src="main.js"></script>
</body>
</html>

main.js (Main Thread): This is the main script running in the main thread.

  • It first checks if the browser supports Web Workers.

  • It then creates a new worker using prime_worker.js.

  • When the user clicks the "Calculate Primes" button, it sends the limit to the worker.

  • It also listens for messages from the worker to display the results.

if (window.Worker) {
    const worker = new Worker('prime_worker.js');

    document.getElementById('startWorker').addEventListener('click', () => {
        const limit = document.getElementById('limit').value;
        worker.postMessage(limit);
    });

    worker.onmessage = (event) => {
        document.getElementById('result').innerText = 'Primes: ' + event.data.join(', ');
    };
} else {
    console.log('Your browser does not support Web Workers.');
}

prime_worker.js (Worker Thread): This is the script that runs in the prime_worker thread.

  • It listens for messages from the main thread.

  • When it receives a message (the limit), it calculates all prime numbers up to that limit.

  • It then sends the list of prime numbers back to the main thread.

onmessage = (event) => {
    const limit = parseInt(event.data);
    const primes = [];
    for (let i = 2; i <= limit; i++) {
        let isPrime = true;
        for (let j = 2; j * j <= i; j++) {
            if (i % j === 0) {
                isPrime = false;
                break;
            }
        }
        if (isPrime) {
            primes.push(i);
        }
    }
    postMessage(primes);
};

How It Works:

  • User Input: The user enters a number in the input field and clicks the "Calculate Primes" button.

  • Message Sent: The main thread sends the entered limit to the worker.

  • Background Calculation: The worker calculates the prime numbers without freezing the UI.

  • Result Received: The worker sends the list of prime numbers back to the main thread.

  • Display Result: The main thread displays the prime numbers on the web page.

  1. Shared Worker Example

addOrSubtract.html (Page 1): This HTML file represents a page where users can perform addition and subtraction operations using a shared worker.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Add or Subtract</title>
</head>
<body>
    <h2>Add or Subtract</h2>
    <label for="num1">Enter number 1:</label>
    <input type="number" id="num1"><br><br>

    <label for="num2">Enter number 2:</label>
    <input type="number" id="num2"><br><br>

    <button onclick="calculate('add')">Add</button>
    <button onclick="calculate('subtract')">Subtract</button>

    <p id="result"></p>

    <script>
        const worker = new SharedWorker('sharedWorker.js');

        // Register ports for operations handled by this page
        worker.port.postMessage({ type: 'register', operation: 'add' });
        worker.port.postMessage({ type: 'register', operation: 'subtract' });

        worker.port.onmessage = function(event) {
            if (event.data.type === 'result') {
                document.getElementById('result').textContent = `Result: ${event.data.value}`;
            }
        };

        function calculate(operation) {
            const num1 = document.getElementById('num1').value;
            const num2 = document.getElementById('num2').value;

            worker.port.postMessage({ type: 'calculate', num1, num2, operation });
        }

        // Clean up when the page is about to unload
        window.addEventListener('beforeunload', function() {
            worker.port.postMessage({ type: 'unregister', operation: 'add' });
            worker.port.postMessage({ type: 'unregister', operation: 'subtract' });
            worker.port.close();
        });
    </script>
</body>
</html>

multiplyOrDivide.html (Page 2): This HTML file represents a page where users can perform multiply and divide operations using a shared worker.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Multiply or Divide</title>
</head>
<body>
    <h2>Multiply or Divide</h2>
    <label for="num1">Enter number 1:</label>
    <input type="number" id="num1"><br><br>

    <label for="num2">Enter number 2:</label>
    <input type="number" id="num2"><br><br>

    <button onclick="calculate('multiply')">Multiply</button>
    <button onclick="calculate('divide')">Divide</button>

    <p id="result"></p>

    <script>
        const worker = new SharedWorker('sharedWorker.js');

        // Register ports for operations handled by this page
        worker.port.postMessage({ type: 'register', operation: 'multiply' });
        worker.port.postMessage({ type: 'register', operation: 'divide' });

        worker.port.onmessage = function(event) {
            if (event.data.type === 'result') {
                document.getElementById('result').textContent = `Result: ${event.data.value}`;
            }
        };

        function calculate(operation) {
            const num1 = document.getElementById('num1').value;
            const num2 = document.getElementById('num2').value;

            worker.port.postMessage({ type: 'calculate', num1, num2, operation });
        }

        // Clean up when the page is about to unload
        window.addEventListener('beforeunload', function() {
            worker.port.postMessage({ type: 'unregister', operation: 'multiply' });
            worker.port.postMessage({ type: 'unregister', operation: 'divide' });
            worker.port.close();
        });
    </script>
</body>
</html>

sharedWorker.js (Shared Worker Thread): This is the script that runs in the sharedWorker thread.

  • It listens for messages from multiple browser tabs/windows.

  • It handles registration, calculation requests, and cleanup tasks based on received messages.

let ports = {};

onconnect = function(event) {
    const port = event.ports[0];

    port.onmessage = function(event) {
        const message = event.data;

        if (message.type === 'register') {
            // Register the port with the specified operation type
            const operation = message.operation;
            ports[operation] = port;
        } else if (message.type === 'calculate') {
            // Perform calculation based on the operation type
            const operation = message.operation;
            const num1 = parseFloat(message.num1);
            const num2 = parseFloat(message.num2);
            let result;

            switch (operation) {
                case 'add':
                    result = num1 + num2;
                    break;
                case 'subtract':
                    result = num1 - num2;
                    break;
                case 'multiply':
                    result = num1 * num2;
                    break;
                case 'divide':
                    result = num2 !== 0 ? num1 / num2 : 'Error: Division by zero';
                    break;
                default:
                    result = 'Error: Invalid operation';
                    break;
            }

            // Send the result back through the appropriate port
            if (ports[operation]) {
                ports[operation].postMessage({ type: 'result', value: result });
            }
        } else if (message.type === 'unregister') {
            // Unregister the port for the specified operation type
            const operation = message.operation;
            if (ports[operation]) {
                delete ports[operation]; // Remove from the ports object
            } 
        }
    };
};

How It Works:

  • Initialization: Both pages (addOrSubtract.html and multiplyOrDivide.html) create a SharedWorker instance pointing to sharedWorker.js.

  • Port Registration: Each page registers specific operations (add, subtract in addOrSubtract.html; multiply, divide in multiplyOrDivide.html) by sending register messages to the shared worker.

  • Message Handling: The shared worker listens for calculate messages, performs the requested calculation, and sends the result back to the appropriate port on the page.

  • Cleanup: When a page is about to unload (beforeunload event), it sends unregister messages to the shared worker and closes the associated port to release resources.

  1. Best Use Cases for Web Workers

  • Heavy Calculations: Performing complex calculations or data processing without freezing the UI.

  • Background Operations: Handling tasks like image or file processing in the background.

  • Real-time Updates: Managing WebSocket connections or updating data from a server without blocking user interaction.

  1. Limitations of Web Workers

  • No DOM Access: They cannot directly manipulate the DOM, limiting their use for tasks requiring UI updates.

  • Limited Browser Support: Although widely supported, some older browsers may have limited or no support for certain features.

Something that came across my mind while learning about web workers—and I think you might have wondered about this too—is how many web workers can actually be created?

I tried to find exact numbers, but I couldn't. What I did discover is that several factors affect how web workers operate. The actual number of web workers that can be effectively used depends on the user's system resources, such as CPU cores and available memory. Each web worker consumes system resources, so the total number that can run simultaneously depends on the capacity of the user's device.

A variety of functions supported by web workers can be explored here on the Mozilla Developer Network.

In addition to dedicated workers and shared workers, there are two more types of workers: Service Workers and Audio Worklet , which I will discuss in a future blog post.

I hope you have all enjoyed the blog and gained insights into the Web Worker API—its functionality, implementation details, limitations, and practical use cases. I encourage you to implement it wherever applicable in your projects.

1
Subscribe to my newsletter

Read articles from Murali Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Murali Singh
Murali Singh

Hi there! I'm Murali Singh, a junior full-stack developer passionate about building web and mobile applications. Let's build something awesome together!