Skip to main content

JavaScript Web Workers

Introduction

JavaScript in browsers traditionally operates in a single-threaded environment, which means it can only execute one operation at a time. This can become problematic when running computationally intensive tasks, as they can block the main thread and freeze the user interface, resulting in an unresponsive web application.

Web Workers provide a solution to this limitation by allowing you to run JavaScript code in background threads that operate independently from the main execution thread. This enables parallel processing without affecting the user interface, leading to smoother, more responsive web applications.

What Are Web Workers?

Web Workers are a feature of modern browsers that allow JavaScript to run scripts in background threads. The worker thread can perform tasks without interfering with the user interface. You can think of Web Workers as separate JavaScript environments running alongside your main script.

Key characteristics of Web Workers include:

  • They run in a separate thread, isolated from the main execution thread
  • They don't have access to the DOM, window, or document objects
  • They communicate with the main thread using a messaging system
  • They can perform computationally intensive tasks without blocking UI rendering

Types of Web Workers

There are three main types of Web Workers:

  1. Dedicated Workers: Used by a single script instance
  2. Shared Workers: Can be shared between multiple scripts or windows
  3. Service Workers: Act as proxy servers that sit between web applications, the browser, and the network

In this tutorial, we'll focus primarily on Dedicated Workers as they're the most commonly used type.

Creating a Basic Web Worker

Let's start with a simple example of creating and using a Web Worker:

Step 1: Create a Worker File

First, create a separate JavaScript file for your worker. Let's call it worker.js:

javascript
// worker.js
self.addEventListener('message', function(e) {
const receivedData = e.data;

// Perform some computation
const result = receivedData * 2;

// Send the result back to the main thread
self.postMessage(result);
});

Step 2: Create and Use the Worker in Your Main Script

Now, in your main JavaScript file, you can create and interact with the worker:

javascript
// main.js
// Create a new worker
const myWorker = new Worker('worker.js');

// Send a message to the worker
myWorker.postMessage(10);

// Listen for messages from the worker
myWorker.onmessage = function(e) {
console.log('Result from worker: ' + e.data); // Output: Result from worker: 20
document.getElementById('result').textContent = e.data;
};

Step 3: Add the HTML Structure

html
<!DOCTYPE html>
<html>
<head>
<title>Web Worker Example</title>
</head>
<body>
<h1>Web Worker Demo</h1>
<p>Result: <span id="result">Waiting...</span></p>

<script src="main.js"></script>
</body>
</html>

Communication Between Workers and the Main Thread

Web Workers communicate with the main thread through a messaging system using the postMessage() method and the onmessage event handler.

Sending Messages

To send a message to a worker:

javascript
// From main thread to worker
myWorker.postMessage(data);

// From worker to main thread
self.postMessage(result);

You can send various types of data including:

  • Primitive values (strings, numbers, booleans)
  • Arrays
  • Objects
  • Transferable objects (like ArrayBuffer)

Receiving Messages

To receive messages:

javascript
// In the main thread
myWorker.onmessage = function(e) {
const receivedData = e.data;
// Process the data
};

// In the worker thread
self.addEventListener('message', function(e) {
const receivedData = e.data;
// Process the data
});

Error Handling in Web Workers

Handling errors in Web Workers is important to ensure your application remains stable:

javascript
// In the main thread
myWorker.onerror = function(error) {
console.error('Worker error:', error.message);
// Handle the error appropriately
};

// In the worker thread
self.addEventListener('error', function(e) {
self.postMessage({ error: e.message });
});

Terminating Web Workers

When you're done with a worker, it's a good practice to terminate it to free up resources:

javascript
// To terminate a worker from the main thread
myWorker.terminate();

// To make a worker terminate itself
self.close();

Practical Example: Heavy Computation

Let's create a more practical example where we use a Web Worker to perform a computationally intensive task: calculating prime numbers.

Worker File (prime-worker.js)

javascript
// prime-worker.js
function isPrime(num) {
if (num <= 1) return false;
if (num <= 3) return true;

if (num % 2 === 0 || num % 3 === 0) return false;

let i = 5;
while (i * i <= num) {
if (num % i === 0 || num % (i + 2) === 0) return false;
i += 6;
}

return true;
}

function findPrimesInRange(start, end) {
const primes = [];

for (let i = start; i <= end; i++) {
if (isPrime(i)) {
primes.push(i);

// Periodically report progress
if (primes.length % 1000 === 0) {
self.postMessage({
type: 'progress',
primes: primes.length,
current: i,
max: end
});
}
}
}

return primes;
}

self.addEventListener('message', function(e) {
const { start, end } = e.data;

const startTime = performance.now();
const primes = findPrimesInRange(start, end);
const endTime = performance.now();

self.postMessage({
type: 'result',
primes: primes,
count: primes.length,
time: endTime - startTime
});
});

Main Script

javascript
// main.js
document.getElementById('startButton').addEventListener('click', function() {
const min = parseInt(document.getElementById('minRange').value) || 1;
const max = parseInt(document.getElementById('maxRange').value) || 100000;

// Update UI
document.getElementById('status').textContent = 'Calculating...';
document.getElementById('progress').style.width = '0%';
document.getElementById('result').textContent = '';

// Create worker
const primeWorker = new Worker('prime-worker.js');

// Send data to worker
primeWorker.postMessage({start: min, end: max});

// Listen for messages
primeWorker.onmessage = function(e) {
const data = e.data;

if (data.type === 'progress') {
// Update progress
const percentage = Math.round((data.current - min) / (max - min) * 100);
document.getElementById('progress').style.width = percentage + '%';
document.getElementById('status').textContent = `Found ${data.primes} primes so far...`;
}
else if (data.type === 'result') {
// Display results
document.getElementById('status').textContent = 'Completed!';
document.getElementById('progress').style.width = '100%';
document.getElementById('result').textContent =
`Found ${data.count} primes between ${min} and ${max} in ${(data.time / 1000).toFixed(2)} seconds`;

// Terminate the worker
primeWorker.terminate();
}
};

// Handle errors
primeWorker.onerror = function(error) {
document.getElementById('status').textContent = 'Error: ' + error.message;
primeWorker.terminate();
};
});

HTML Structure

html
<!DOCTYPE html>
<html>
<head>
<title>Prime Number Calculator with Web Worker</title>
<style>
.progress-bar {
width: 100%;
background-color: #f0f0f0;
border-radius: 4px;
margin: 10px 0;
}
#progress {
height: 20px;
background-color: #4caf50;
width: 0%;
border-radius: 4px;
transition: width 0.3s;
}
</style>
</head>
<body>
<h1>Prime Number Calculator</h1>
<div>
<label for="minRange">Minimum number:</label>
<input type="number" id="minRange" value="1" min="1">

<label for="maxRange">Maximum number:</label>
<input type="number" id="maxRange" value="100000" min="1">

<button id="startButton">Calculate Primes</button>
</div>

<div class="progress-bar">
<div id="progress"></div>
</div>

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

<script src="main.js"></script>
</body>
</html>

Working with Data in Web Workers

Shared Memory with SharedArrayBuffer

In modern browsers with proper security settings, you can use SharedArrayBuffer to share memory between the main thread and a worker:

javascript
// In the main thread
const buffer = new SharedArrayBuffer(4);
const view = new Int32Array(buffer);
view[0] = 42;

const worker = new Worker('worker.js');
worker.postMessage({ sharedBuffer: buffer });

// In worker.js
self.onmessage = function(e) {
const view = new Int32Array(e.data.sharedBuffer);
console.log(view[0]); // Outputs: 42

// Modify the shared data
view[0] = 100;
};

// Back in main thread (after the worker has processed)
setTimeout(() => {
console.log(view[0]); // Will output: 100
}, 100);

Transferable Objects

For large data transfers, you can use transferable objects to move ownership of an object to a worker without copying it:

javascript
// Create a large array buffer
const buffer = new ArrayBuffer(1024 * 1024); // 1MB buffer

// Transfer it to the worker (second argument specifies transferable objects)
worker.postMessage({ data: buffer }, [buffer]);

// After transfer, the buffer is no longer usable in the main thread
console.log(buffer.byteLength); // 0 - buffer has been transferred

Limitations of Web Workers

While Web Workers are powerful, they do have limitations:

  1. No DOM Access: Workers cannot directly manipulate the DOM
  2. Limited Window Access: No access to the window object, document, or parent objects
  3. Same-Origin Restriction: Workers are subject to the same-origin policy
  4. Limited Sharing: Passing data between the main thread and workers involves copying the data (unless using transferable objects)
  5. Resource Usage: Each worker creates an additional thread, consuming memory and resources

When to Use Web Workers

Web Workers are particularly useful for:

  • Computationally intensive tasks: Complex calculations, data processing, or algorithms
  • Parsing large data sets: JSON parsing, data manipulation, or searching through large arrays
  • Network requests in background: Polling or making multiple fetch requests without blocking the UI
  • Image/video processing: Applying filters, encoding/decoding, or analysis
  • Simulations and games: Physics calculations, AI algorithms, or procedural generation

Best Practices

  1. Keep Workers Focused: Each worker should have a specific purpose
  2. Minimize Data Transfer: Large data transfers between the main thread and workers can impact performance
  3. Error Handling: Always implement proper error handling for your workers
  4. Clean Up: Terminate workers when they're no longer needed
  5. Feature Detection: Check if the browser supports Web Workers before using them:
javascript
if (typeof Worker !== 'undefined') {
// Web Workers are supported
const myWorker = new Worker('worker.js');
} else {
// Fallback for browsers that don't support Web Workers
console.log('Web Workers not supported in this browser');
}

Summary

Web Workers provide a powerful way to run JavaScript code in parallel with the main thread, enabling better performance and responsiveness in web applications. They use a message-based communication system to exchange data between the main thread and worker threads, allowing you to offload intensive operations without freezing the user interface.

Key takeaways from this tutorial:

  • Web Workers run scripts in background threads, separate from the main execution thread
  • Workers communicate with the main thread through messages using postMessage() and onmessage
  • Workers cannot directly access the DOM or window object
  • They're ideal for computationally intensive tasks like data processing and complex calculations
  • Use proper error handling and terminate workers when no longer needed

By incorporating Web Workers into your JavaScript applications, you can create smoother, more responsive user experiences that better utilize modern multi-core processors.

Additional Resources

Exercises

  1. Create a Web Worker that calculates the Fibonacci sequence up to a number specified by the main thread.
  2. Build a simple image filter application that uses a Web Worker to apply filters to an image without freezing the UI.
  3. Implement a word-counting application that uses a Web Worker to analyze text and count words, characters, and sentences.
  4. Create a sorting visualization that compares sorting algorithms running on the main thread vs. in a Web Worker.
  5. Build a Web Worker that performs a network request and processes the results before sending them back to the main thread.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)