Understanding and Implementing Web Workers in JavaScript Applications

Web Workers

JavaScript is a single-threaded language, and when browsers need to perform computationally intensive tasks, other operations on the page can become unresponsive due to the main thread being occupied. This creates a poor user experience. For example, when displaying large BIM models in the browser, the frontend needs to fetch and parse various data including vertices, colors, and attributes from standard model formats, which can cause noticeable delays.

Web Workers excel at handling time-consuming computations, large data processing, and other CPU-intensive tasks, improving overall performance and user experience. However, they're not suitable for all scenarios, particularly those involving direct DOM manipulation.

Key Concepts of Web Workers:

  • Thread Model: Traditional browsers run JavaScript in the main thread, which handles user interface and interaction tasks. Web Workers create additional threads that run in the background, independent of the main thread.
  • Independent Global Context: Each Web Worker has its own global context, separate from the main thread's context. Variables and functions defined in a Worker don't affect the main thread and vice versa.
  • Communication: Main thread and Web Workers communicate via messages. The postMessage method sends messages, establishing bidirectional communication. Message passing occurs through copying rather than sharing objects, ensuring data security.
  • No DOM Access: Web Workers cannot directly access the DOM, meaning they can't manipulate page elements directly. They're primarily for computational tasks rather than UI interactions.
  • Network Requests: Web Workers can perform asynchronous operations including network requests without blocking the main thread.
  • Lifecycle: Web Workers have their own lifecycle, with event listeners (like onmessage and onerror) to capture relevant events. When no longer needed, they can be terminated using the terminate method.
  • Limitations: Since Web Workers run in separate threads, they cannot directly access main thread variables and functions. Communication through message passing introduces some data copying overhead.

Console Inspection

Usage Considerations

  • Same-Origin Restriction: Worker scripts and main process scripts must follow same-origin restrictions. Their path protocols, domains, and port numbers must match.
  • Interface Limitations: Some window-scope methods are unavailable, such as DOM objects, window.alert, and window.confirm. Refer to Supported Web APIs for available features.
  • File Restrictions: Cannot load local JS files; must use online resources.
  • Remember to Close: Workers consume system resources, so they should be terminated after completing their tasks. In the parent process: worker.terminate(); In the worker process: self.close();

Message Passing

Main thread and workers communicate through message passing. For both main thread and workers:

  • Use postMessage to send messages
  • Use onmessage to receive messages
  • Use onerror to listen for error events

In the main thread, onmessage, onerror, and postMessage must be attached to the worker object. In the worker, use self.onmessage, self.postMessage, and self.onerror, or omit self since it refers to the worker itself.

Creating Sub-workers

Create multiple sub-workers within a worker to handle different tasks. Note that sub-workers must follow the same-origin restriction as parent workers.

Practical Web Worker Implementation

Main Page

<script>
document.addEventListener('DOMContentLoaded', () => {
    // Create a dedicated worker
    const calculationWorker = new Worker('calculation-worker.js');
    
    // Send data to the worker
    calculationWorker.postMessage(100); // Calculate 100th Fibonacci number
    
    // Listen for messages from the worker
    calculationWorker.onmessage = (event) => {
        const result = event.data;
        console.log('Worker returned result:', result);
        calculationWorker.terminate();
    };
    
    // Error handling
    calculationWorker.onerror = (error) => {
        console.error('Worker error:', error);
    };
    
    // Multiple workers example
    const dataProcessingWorker = new Worker('data-processor.js');
    dataProcessingWorker.postMessage({ operation: 'transform', data: largeDataSet });
    
    dataProcessingWorker.onmessage = (event) => {
        const processedData = event.data;
        console.log('Data processing complete:', processedData);
        dataProcessingWorker.terminate();
    };
});
</script>

Worker Script (calculation-worker.js)

self.onmessage = function(event) {
    const inputData = event.data;
    const result = performIntensiveCalculation(inputData);
    self.postMessage(result);
};

function performIntensiveCalculation(n) {
    let result = 0;
    for (let i = 0; i <= n; i++) {
        result += i;
    }
    return result;
}

Important Notes

  • Worker scripts should be placed in the public folder to ensure they're accessible after bundling
  • For large or complex data, use JSON.stringify() before sending and JSON.parse() in the worker
  • If TypeScript causes issues, switch to JavaScript

The following image demonstrates converting [{}...] data format to a two-dimensional array. However, with hundreds of thousands of entries, processing on the main thread causes significant lag, necessitating additional threads.

Shared Workers

The worker creation method described above is categorized as a dedicated worker on MDN. Another type is the shared worker, which serves similar purposes but allows common methods to be reused across different contexts. SharedWorkers can:

  • Be used across different HTML pages
  • Be used between main windows and iframes
  • Allow multiple workers to access data or methods defined in the shared worker

Creation Method:

// Create SharedWorker in two same-origin pages using the same script
const sharedWorker = new SharedWorker("shared-worker.js");

Main Differences from Dedicated Workers:

In shared worker environments, both main process and worker message handling occur through port objects:

sharedWorker.port.postMessage([firstValue, secondValue]);

After connecting a shared worker port, both page main processes send messages to the worker. Event listening in the worker should be placed in the onconnect event:

onconnect = function(e) {
  const port = e.ports[0];
  
  port.onmessage = function(e) {
    const workerResult = 'Result: ' + (e.data[0] * e.data[1]);
    port.postMessage(workerResult);
  }
  port.start();
}

When using addEventListener to listen for worker messages, the main process must call myWorker.port.start() to activate the port. With onmessage, this activation isn't necesary.

sharedWorker.port.addEventListener('message', function(e) {
    console.log('Message received from worker');
});
sharedWorker.port.start();

Use port.onmessage and port.postMessage within onconnect for message hendling.

Main process uses port.postMessage() and port.onmessage for worker communication.

Example:

Note: Server must be running

index.html


<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Shared Worker Demo</title>
</head>
<body>
    <div>
        <button id="increment">Increment</button>
        <h1 id="counter">0</h1>
    </div>
    <script>
        const sharedWorker = new SharedWorker('shared-worker.js');
        
        sharedWorker.port.postMessage({ action: 'init', id: 'main' });
        
        sharedWorker.port.onmessage = function(e) {
            document.getElementById('counter').textContent = e.data.value;
        };
        
        document.getElementById('increment').addEventListener('click', () => {
            const current = parseInt(document.getElementById('counter').textContent);
            sharedWorker.port.postMessage({ action: 'increment', id: 'main', value: current + 1 });
        });
    </script>
</body>
</html>

index2.html


<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Secondary Page</title>
</head>
<body>
    <h1 id="secondary-counter">0</h1>
    <script>
        const sharedWorker = new SharedWorker('shared-worker.js');
        
        sharedWorker.port.postMessage({ action: 'init', id: 'secondary' });
        
        sharedWorker.port.onmessage = function(e) {
            document.getElementById('secondary-counter').textContent = e.data.value;
        };
    </script>
</body>
</html>

shared-worker.js

const connectedPorts = [];
const portIdentifiers = [];

onconnect = function(e) {
    const port = e.ports[0];
    
    port.addEventListener('message', function(event) {
        const message = event.data;
        
        if (message.action === 'init') {
            const portIndex = portIdentifiers.indexOf(message.id);
            if (portIndex === -1) {
                connectedPorts.push(port);
                portIdentifiers.push(message.id);
            } else {
                connectedPorts[portIndex].close();
                connectedPorts[portIndex] = port;
            }
        } else if (message.action === 'increment') {
            broadcastToAllPorts(message);
        }
    });
    
    port.start();
};

function broadcastToAllPorts(data) {
    connectedPorts.forEach(port => {
        port.postMessage({ value: data.value });
    });
}

Tags: Web Workers javascript multithreading performance optimization Shared Workers

Posted on Mon, 07 Sep 2026 16:17:54 +0000 by doctor_james