Skip to content

HTML5 - Web Workers

JavaScript in web browsers traditionally runs in a single thread, often referred to as the ‘main thread’. This thread handles user interface updates (rendering, responding to clicks, scrolling) as well as executing JavaScript code. If a script performs a computationally intensive task (like complex calculations, processing large amounts of data, or intensive network requests), it can block the main thread, causing the user interface to become unresponsive or ‘freeze’.

Consider this simplified example: clicking the ‘Heavy Task’ button simulates a long-running operation. While it’s running, clicking the ‘Say Hello’ button won’t trigger the alert immediately because the main thread is busy.

<!DOCTYPE html>
<html>
<head>
<title>Blocking Example</title>
<script>
function performHeavyTask() {
console.log('Starting heavy task...');
// Simulate a long-running calculation
const end = Date.now() + 5000; // Run for 5 seconds
while (Date.now() < end) {
// Busy-wait loop
}
console.log('Heavy task completed.');
alert('Heavy task finished!');
}
function sayHello() {
alert('Hello there!');
}
</script>
</head>
<body>
<button onclick="performHeavyTask()">Run Heavy Task (Blocks UI)</button>
<button onclick="sayHello()">Say Hello</button>
</body>
</html>

Web Workers provide a mechanism to run JavaScript code in background threads, separate from the main execution thread. This allows long-running or computationally expensive tasks to be performed without blocking the main thread, keeping the user interface responsive.

Workers are ideal for tasks like complex calculations, data processing, background synchronization, or managing large datasets.

There are different types of workers, including Dedicated Workers (the most common type, tied to a single script context), Shared Workers (can be accessed by multiple scripts from the same origin), and Service Workers (act as proxy servers, managing network requests and enabling offline capabilities and push notifications - a more advanced topic).

Important Limitation: Scripts running in a Web Worker operate in a separate global context (DedicatedWorkerGlobalScope, SharedWorkerGlobalScope, etc.). They do not have direct access to the window object, the DOM (Document Object Model), or many other APIs available in the main thread. Communication between the worker and the main thread happens via message passing.

  1. Create the Worker: In your main script, you create a new Worker object, passing the path to the JavaScript file that contains the worker’s code.
main.js
const myWorker = new Worker('worker.js');
  1. Define Worker Logic: Create the worker.js file. This script will run in the background thread. It typically listens for messages from the main thread and sends messages back with results.
// worker.js
// Listen for messages from the main thread
self.onmessage = function(event) {
console.log('Message received from main script:', event.data);
if (event.data.command === 'calculate') {
// Perform the calculation (e.g., sum numbers)
const result = event.data.numbers.reduce((a, b) => a + b, 0);
// Send the result back to the main thread
self.postMessage({ type: 'result', payload: result });
}
};
// You can also send messages proactively
// self.postMessage('Worker initialized!');
  1. Communicate: Use the postMessage() method to send data (strings, objects, arrays, ArrayBuffers - anything that can be structured cloned) between the main thread and the worker. Listen for messages using the onmessage event handler.
// main.js
const myWorker = new Worker('worker.js');
// Send data to the worker
const dataToSend = { command: 'calculate', numbers: [1, 2, 3, 4, 5] };
myWorker.postMessage(dataToSend);
// Listen for messages from the worker
myWorker.onmessage = function(event) {
console.log('Message received from worker:', event.data);
if (event.data.type === 'result') {
alert(`Calculation result: ${event.data.payload}`);
}
};
  1. Import Scripts (Optional): If your worker logic is split across multiple files, you can use the importScripts() function within the worker script to load them.
worker.js
importScripts('helper.js', 'anotherHelper.js');
// Now functions/variables from helper.js and anotherHelper.js are available
self.onmessage = function(event) {
// ... use imported functions ...
};

Let’s rewrite the initial blocking example using a Web Worker.

index.html (Main Page):

<!DOCTYPE html>
<html>
<head>
<title>Web Worker Example</title>
<script>
let worker;
function startHeavyTaskWorker() {
if (window.Worker) { // Check for browser support
if (!worker) {
worker = new Worker('heavy_task_worker.js');
worker.onmessage = function(event) {
console.log('Message received from worker:', event.data);
alert('Worker finished the heavy task!');
// Optionally terminate the worker if no longer needed
// worker.terminate();
// worker = null;
};
worker.onerror = function(error) {
console.error('Error in worker:', error.message, error);
alert('An error occurred in the worker.');
};
}
console.log('Sending task command to worker...');
worker.postMessage({ command: 'startTask' });
} else {
alert('Your browser does not support Web Workers.');
}
}
function sayHello() {
alert('Hello there!');
}
</script>
</head>
<body>
<button onclick="startHeavyTaskWorker()">Run Heavy Task (Worker)</button>
<button onclick="sayHello()">Say Hello</button>
</body>
</html>

heavy_task_worker.js (Worker Script):

// heavy_task_worker.js
self.onmessage = function(event) {
if (event.data.command === 'startTask') {
console.log('Worker starting heavy task...');
// Simulate a long-running calculation
const end = Date.now() + 5000; // Run for 5 seconds
while (Date.now() < end) {
// Busy-wait loop
}
console.log('Worker heavy task completed.');
self.postMessage({ status: 'completed' });
}
};

Now, when you click ‘Run Heavy Task (Worker)’, the long operation runs in the background. You can click ‘Say Hello’ immediately, and the alert will appear without delay because the main thread is not blocked.

Web Workers continue running until they are explicitly terminated. From the main thread, you can stop a worker using its terminate() method:

main.js
if (worker) {
worker.terminate();
worker = null; // Clear the reference
console.log('Worker terminated.');
}

A worker can also terminate itself by calling self.close() within the worker script.

A terminated worker cannot be restarted; you must create a new instance if needed.

Errors that occur within a worker script (e.g., syntax errors, runtime exceptions) can be caught by attaching an onerror event handler to the Worker object in the main thread. The event object provides information like the error message, filename, and line number.

// main.js
myWorker.onerror = function(error) {
console.error(`Error in worker: ${error.message} at ${error.filename}:${error.lineno}`);
// Handle the error appropriately, e.g., notify the user
};

You can easily check if the browser supports Web Workers by testing for the existence of the Worker object on the window:

if (window.Worker) {
// Web Workers are supported
console.log('Web Workers supported!');
// Proceed to create workers...
} else {
// Web Workers are not supported
console.warn('Web Workers not supported in this browser.');
// Provide fallback or notify the user
}