Web Workers in JavaScript
Web Workers provide a way to run JavaScript in the background, separate from the main browser thread. This allows for multi-threading in JavaScript, enabling CPU-intensive tasks to run without blocking the user interface.
Introduction to Web Workers
JavaScript traditionally runs in a single thread, which means that long-running or complex operations can block the UI and make your application unresponsive. Web Workers solve this problem by allowing you to run scripts in background threads.
Note: Web Workers run in an isolated context. They don't have access to the DOM, the window
object, or the parent
object. They can only communicate with the main thread through messages.
Types of Web Workers
There are three main types of Web Workers:
1. Dedicated Workers
A dedicated worker is the most common type, used by a single script instance. It can only communicate with the script that created it.
2. Shared Workers
A shared worker can be accessed by multiple scripts running in different windows, iframes, or even workers. They're useful for sharing resources between different parts of your application.
3. Service Workers
Service workers act as proxy servers that sit between web applications, the browser, and the network. They're primarily used for features like offline support, push notifications, and background sync.
Creating a Dedicated Worker
To create a dedicated worker, you need two files: the main JavaScript file and a separate worker file.
// Main script (main.js)
// Create a new worker
const worker = new Worker('worker.js');
// Send a message to the worker
worker.postMessage({ command: 'start', data: [1, 2, 3, 4, 5] });
// Receive messages from the worker
worker.onmessage = function(event) {
console.log('Received from worker:', event.data);
};
// Handle errors
worker.onerror = function(error) {
console.error('Worker error:', error.message);
};
// Worker script (worker.js)
// Listen for messages from the main thread
self.onmessage = function(event) {
console.log('Received from main thread:', event.data);
if (event.data.command === 'start') {
// Perform some CPU-intensive task
const result = processData(event.data.data);
// Send the result back to the main thread
self.postMessage({ result: result });
}
};
// Example of a CPU-intensive function
function processData(data) {
// Simulate heavy computation
let result = 0;
for (let i = 0; i < 10000000; i++) {
result += Math.sqrt(i);
}
// Process the actual data
return data.map(x => x * x);
}
Communication Between Workers and the Main Thread
Web Workers communicate with the main thread using a messaging system:
Sending Messages
You can send messages using the postMessage()
method. The message can be any value that can be cloned using the structured clone algorithm, which includes most JavaScript objects and values.
// From main thread to worker
worker.postMessage({
command: 'process',
data: [1, 2, 3, 4, 5],
options: {
multiply: 2,
add: 1
}
});
// From worker to main thread
self.postMessage({
status: 'complete',
result: [3, 5, 7, 9, 11]
});
Receiving Messages
You can receive messages by listening to the message
event:
// In main thread
worker.addEventListener('message', function(event) {
console.log('Received from worker:', event.data);
});
// Alternative syntax
worker.onmessage = function(event) {
console.log('Received from worker:', event.data);
};
// In worker
self.addEventListener('message', function(event) {
console.log('Received from main thread:', event.data);
});
// Alternative syntax
self.onmessage = function(event) {
console.log('Received from main thread:', event.data);
};
Terminating a Worker
You can terminate a worker from the main thread or from within the worker itself:
// From main thread
worker.terminate();
// From within the worker
self.close();
Warning: Terminating a worker will immediately stop all operations. Any pending operations or messages will be lost.
Importing Scripts in Workers
Workers can import additional scripts using the importScripts()
function:
// In worker.js
importScripts('helper1.js', 'helper2.js');
// Now you can use functions and variables defined in those scripts
const result = helperFunction(data);
Shared Workers
Shared Workers allow multiple scripts to communicate with a single worker. This is useful for sharing resources between different parts of your application.
// Main script (in multiple pages or iframes)
const sharedWorker = new SharedWorker('shared-worker.js');
// Get a reference to the port
const port = sharedWorker.port;
// Start the port
port.start();
// Send a message to the shared worker
port.postMessage({ command: 'register', id: 'page1' });
// Receive messages from the shared worker
port.onmessage = function(event) {
console.log('Received from shared worker:', event.data);
};
// Shared worker script (shared-worker.js)
const connections = [];
// Listen for connections
self.onconnect = function(event) {
const port = event.ports[0];
connections.push(port);
// Start the port
port.start();
// Listen for messages on this port
port.onmessage = function(event) {
console.log('Shared worker received:', event.data);
if (event.data.command === 'broadcast') {
// Broadcast a message to all connected ports
connections.forEach(connection => {
connection.postMessage({
type: 'broadcast',
message: event.data.message,
from: event.data.id
});
});
}
};
};
Transferable Objects
For large data transfers, you can use transferable objects to avoid the cost of copying the data. When you transfer an object, it becomes unusable in the sender's context.
// Create a large array buffer
const buffer = new ArrayBuffer(100 * 1024 * 1024); // 100MB
// Fill the buffer with data
const view = new Uint8Array(buffer);
for (let i = 0; i < view.length; i++) {
view[i] = i % 256;
}
// Transfer the buffer to the worker (not copying it)
worker.postMessage({ buffer: buffer }, [buffer]);
// After the transfer, buffer is neutered (zero length)
console.log(buffer.byteLength); // 0
Worker Scope and Limitations
Web Workers have access to a limited set of JavaScript features:
Available Features
- The
navigator
object - The
location
object (read-only) XMLHttpRequest
andfetch
setTimeout
/setInterval
- The Application Cache
- Importing external scripts with
importScripts()
- Creating other workers
- WebSockets and IndexedDB
Unavailable Features
- The DOM (document, window, etc.)
- The
parent
object - The
localStorage
andsessionStorage
objects - Access to the main thread's global variables
Practical Examples
Example 1: Image Processing
Web Workers are ideal for image processing tasks, which can be CPU-intensive:
// Main script
const worker = new Worker('image-processor.js');
// Get the image data from a canvas
const canvas = document.getElementById('sourceCanvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Send the image data to the worker
worker.postMessage({
imageData: imageData,
filter: 'grayscale'
});
// Receive the processed image data
worker.onmessage = function(event) {
// Draw the processed image data on another canvas
const resultCanvas = document.getElementById('resultCanvas');
const resultCtx = resultCanvas.getContext('2d');
resultCtx.putImageData(event.data.imageData, 0, 0);
};
// image-processor.js
self.onmessage = function(event) {
const imageData = event.data.imageData;
const filter = event.data.filter;
// Apply the requested filter
switch(filter) {
case 'grayscale':
applyGrayscale(imageData);
break;
case 'invert':
applyInvert(imageData);
break;
// Add more filters as needed
}
// Send the processed image data back
self.postMessage({ imageData: imageData });
};
function applyGrayscale(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // Red
data[i + 1] = avg; // Green
data[i + 2] = avg; // Blue
// data[i + 3] is Alpha (unchanged)
}
}
function applyInvert(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // Red
data[i + 1] = 255 - data[i + 1]; // Green
data[i + 2] = 255 - data[i + 2]; // Blue
// data[i + 3] is Alpha (unchanged)
}
}
Example 2: Data Processing
Web Workers can be used for processing large datasets without blocking the UI:
// Main script
const worker = new Worker('data-processor.js');
// Fetch a large dataset
fetch('large-dataset.json')
.then(response => response.json())
.then(data => {
// Send the data to the worker
worker.postMessage({
command: 'analyze',
data: data
});
// Update UI to show processing status
document.getElementById('status').textContent = 'Processing...';
});
// Receive the processed results
worker.onmessage = function(event) {
// Update the UI with the results
document.getElementById('status').textContent = 'Complete';
document.getElementById('results').textContent = JSON.stringify(event.data.results, null, 2);
};
// data-processor.js
self.onmessage = function(event) {
if (event.data.command === 'analyze') {
const data = event.data.data;
// Perform complex analysis
const results = analyzeData(data);
// Send the results back
self.postMessage({
status: 'complete',
results: results
});
}
};
function analyzeData(data) {
// Example: Calculate statistics for a dataset
const results = {
count: data.length,
averages: {},
totals: {},
min: {},
max: {}
};
// Skip if no data
if (data.length === 0) return results;
// Get all numeric properties from the first item
const numericProps = Object.keys(data[0]).filter(key =>
typeof data[0][key] === 'number'
);
// Initialize results
numericProps.forEach(prop => {
results.totals[prop] = 0;
results.min[prop] = Infinity;
results.max[prop] = -Infinity;
});
// Process each item
data.forEach(item => {
numericProps.forEach(prop => {
results.totals[prop] += item[prop];
results.min[prop] = Math.min(results.min[prop], item[prop]);
results.max[prop] = Math.max(results.max[prop], item[prop]);
});
});
// Calculate averages
numericProps.forEach(prop => {
results.averages[prop] = results.totals[prop] / data.length;
});
return results;
}
Interactive Example
Try out this interactive example to see Web Workers in action:
Best Practices
- Use workers for CPU-intensive tasks: Image processing, data analysis, complex calculations
- Keep the main thread free for UI updates: Move non-UI work to workers
- Be mindful of communication overhead: Transferring large amounts of data between threads can be expensive
- Use transferable objects for large data:
ArrayBuffer
,MessagePort
,ImageBitmap
- Terminate workers when they're no longer needed: This frees up resources
- Handle errors: Always implement error handling for your workers
- Consider a worker pool: For multiple tasks, reuse workers instead of creating new ones
Browser Support
Web Workers are supported in all modern browsers, including:
- Chrome 4+
- Firefox 3.5+
- Safari 4+
- Edge 12+
- Opera 10.6+
Next Steps
Now that you understand Web Workers, you might want to explore:
- Service Workers for offline applications and push notifications
- Worklets for specialized rendering tasks
- WebAssembly for high-performance computing in the browser
- Worker libraries like Comlink for easier communication