Browser APIsFeatured
JavaScript Web Workers: Background Processing Guide
Master Web Workers in JavaScript for background processing. Learn parallel computing, worker types, and performance optimization.
By JavaScriptDoc Team•
web workersjavascriptperformanceparallelbrowser
JavaScript Web Workers: Background Processing Guide
Web Workers enable JavaScript applications to run scripts in background threads, allowing for parallel processing without blocking the main UI thread. This guide covers everything you need to know about Web Workers.
Understanding Web Workers
Web Workers make it possible to run scripts in background threads. The worker thread can perform tasks without interfering with the user interface.
// Main thread
if (typeof Worker !== 'undefined') {
// Create a new worker
const myWorker = new Worker('worker.js');
// Send data to worker
myWorker.postMessage({ cmd: 'start', msg: 'Hello Worker' });
// Receive messages from worker
myWorker.onmessage = function (e) {
console.log('Message from worker:', e.data);
};
// Handle errors
myWorker.onerror = function (error) {
console.error('Worker error:', error);
};
} else {
console.log('Web Workers not supported');
}
// worker.js
self.onmessage = function (e) {
const data = e.data;
switch (data.cmd) {
case 'start':
self.postMessage('Worker started: ' + data.msg);
break;
case 'stop':
self.postMessage('Worker stopped');
self.close(); // Terminates the worker
break;
}
};
Types of Web Workers
Dedicated Workers
// dedicated-worker.js
let count = 0;
// Listen for messages
self.addEventListener('message', function (e) {
const { action, data } = e.data;
switch (action) {
case 'increment':
count += data;
self.postMessage({ type: 'count', value: count });
break;
case 'calculate':
const result = performHeavyCalculation(data);
self.postMessage({ type: 'result', value: result });
break;
case 'fetch':
fetchData(data.url).then((result) => {
self.postMessage({ type: 'fetched', data: result });
});
break;
}
});
function performHeavyCalculation(n) {
// Simulate heavy computation
let result = 0;
for (let i = 0; i < n; i++) {
result += Math.sqrt(i) * Math.random();
}
return result;
}
async function fetchData(url) {
const response = await fetch(url);
return response.json();
}
// Main thread usage
class WorkerManager {
constructor(workerPath) {
this.worker = new Worker(workerPath);
this.callbacks = new Map();
this.taskId = 0;
this.worker.onmessage = (e) => {
const { id, type, data } = e.data;
const callback = this.callbacks.get(id);
if (callback) {
callback(data);
this.callbacks.delete(id);
}
};
}
postTask(action, data) {
return new Promise((resolve) => {
const id = this.taskId++;
this.callbacks.set(id, resolve);
this.worker.postMessage({ id, action, data });
});
}
terminate() {
this.worker.terminate();
}
}
Shared Workers
// shared-worker.js
const connections = [];
self.addEventListener('connect', function (e) {
const port = e.ports[0];
connections.push(port);
port.addEventListener('message', function (e) {
const { action, data } = e.data;
switch (action) {
case 'broadcast':
// Send to all connected ports
connections.forEach((p) => {
p.postMessage({ type: 'broadcast', data: data });
});
break;
case 'getConnectionCount':
port.postMessage({
type: 'connectionCount',
count: connections.length,
});
break;
}
});
port.start(); // Required for addEventListener
// Handle disconnection
port.addEventListener('close', function () {
const index = connections.indexOf(port);
if (index !== -1) {
connections.splice(index, 1);
}
});
});
// Using shared worker in main thread
if (typeof SharedWorker !== 'undefined') {
const sharedWorker = new SharedWorker('shared-worker.js');
sharedWorker.port.start();
sharedWorker.port.postMessage({
action: 'broadcast',
data: 'Hello from tab ' + Date.now(),
});
sharedWorker.port.onmessage = function (e) {
console.log('Received:', e.data);
};
}
Data Transfer and Communication
Transferable Objects
// Efficient data transfer with transferable objects
class ImageProcessor {
constructor() {
this.worker = new Worker('image-processor-worker.js');
}
processImage(imageData) {
return new Promise((resolve) => {
// Create a copy of the image data
const buffer = imageData.data.buffer;
const copy = buffer.slice(0);
this.worker.onmessage = (e) => {
resolve(
new ImageData(
new Uint8ClampedArray(e.data.buffer),
e.data.width,
e.data.height
)
);
};
// Transfer ownership of the buffer to the worker
this.worker.postMessage(
{
buffer: copy,
width: imageData.width,
height: imageData.height,
},
[copy]
); // Transferable list
});
}
}
// image-processor-worker.js
self.onmessage = function (e) {
const { buffer, width, height } = e.data;
const data = new Uint8ClampedArray(buffer);
// Process the image
for (let i = 0; i < data.length; i += 4) {
// Example: Convert to grayscale
const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
data[i] = gray; // Red
data[i + 1] = gray; // Green
data[i + 2] = gray; // Blue
// Alpha channel unchanged
}
// Transfer back
self.postMessage(
{
buffer: data.buffer,
width,
height,
},
[data.buffer]
);
};
// SharedArrayBuffer for shared memory (where available)
if (typeof SharedArrayBuffer !== 'undefined') {
class SharedMemoryWorker {
constructor(workerCount) {
this.workerCount = workerCount;
this.workers = [];
// Create shared memory
const sharedBuffer = new SharedArrayBuffer(1024 * 1024); // 1MB
this.sharedArray = new Float32Array(sharedBuffer);
// Create workers
for (let i = 0; i < workerCount; i++) {
const worker = new Worker('shared-memory-worker.js');
worker.postMessage({
cmd: 'init',
sharedBuffer,
workerId: i,
workerCount,
});
this.workers.push(worker);
}
}
process(data) {
// Copy data to shared memory
this.sharedArray.set(data);
// Start all workers
const promises = this.workers.map((worker, index) => {
return new Promise((resolve) => {
worker.onmessage = () => resolve();
worker.postMessage({ cmd: 'process', length: data.length });
});
});
return Promise.all(promises).then(() => {
// Return processed data
return Array.from(this.sharedArray.slice(0, data.length));
});
}
}
}
Worker Pools
Implementing a Worker Pool
class WorkerPool {
constructor(workerScript, poolSize = navigator.hardwareConcurrency || 4) {
this.workerScript = workerScript;
this.poolSize = poolSize;
this.workers = [];
this.freeWorkers = [];
this.queue = [];
this.init();
}
init() {
for (let i = 0; i < this.poolSize; i++) {
const worker = new Worker(this.workerScript);
worker.id = i;
worker.isBusy = false;
worker.onmessage = (e) => {
const task = worker.currentTask;
if (task) {
task.resolve(e.data);
worker.currentTask = null;
}
worker.isBusy = false;
this.freeWorkers.push(worker);
// Process next task in queue
this.processQueue();
};
worker.onerror = (error) => {
const task = worker.currentTask;
if (task) {
task.reject(error);
worker.currentTask = null;
}
worker.isBusy = false;
this.freeWorkers.push(worker);
this.processQueue();
};
this.workers.push(worker);
this.freeWorkers.push(worker);
}
}
execute(data) {
return new Promise((resolve, reject) => {
const task = { data, resolve, reject };
if (this.freeWorkers.length > 0) {
this.assignTask(task);
} else {
this.queue.push(task);
}
});
}
assignTask(task) {
const worker = this.freeWorkers.pop();
worker.isBusy = true;
worker.currentTask = task;
worker.postMessage(task.data);
}
processQueue() {
if (this.queue.length > 0 && this.freeWorkers.length > 0) {
const task = this.queue.shift();
this.assignTask(task);
}
}
terminate() {
this.workers.forEach((worker) => worker.terminate());
this.workers = [];
this.freeWorkers = [];
this.queue = [];
}
getStats() {
return {
total: this.workers.length,
busy: this.workers.filter((w) => w.isBusy).length,
free: this.freeWorkers.length,
queued: this.queue.length,
};
}
}
// Usage example
const pool = new WorkerPool('calculation-worker.js', 4);
// Process multiple tasks
const tasks = [];
for (let i = 0; i < 100; i++) {
tasks.push(
pool.execute({
operation: 'calculate',
value: i,
})
);
}
Promise.all(tasks).then((results) => {
console.log('All tasks completed:', results);
pool.terminate();
});
Practical Use Cases
Image Processing
// image-filter-worker.js
const filters = {
blur(imageData, radius = 5) {
const { data, width, height } = imageData;
const output = new Uint8ClampedArray(data);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let r = 0,
g = 0,
b = 0,
a = 0;
let count = 0;
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
const ny = y + dy;
const nx = x + dx;
if (ny >= 0 && ny < height && nx >= 0 && nx < width) {
const idx = (ny * width + nx) * 4;
r += data[idx];
g += data[idx + 1];
b += data[idx + 2];
a += data[idx + 3];
count++;
}
}
}
const idx = (y * width + x) * 4;
output[idx] = r / count;
output[idx + 1] = g / count;
output[idx + 2] = b / count;
output[idx + 3] = a / count;
}
}
return output;
},
brightness(imageData, adjustment) {
const { data } = imageData;
const output = new Uint8ClampedArray(data);
for (let i = 0; i < data.length; i += 4) {
output[i] = Math.min(255, Math.max(0, data[i] + adjustment));
output[i + 1] = Math.min(255, Math.max(0, data[i + 1] + adjustment));
output[i + 2] = Math.min(255, Math.max(0, data[i + 2] + adjustment));
}
return output;
},
contrast(imageData, adjustment) {
const { data } = imageData;
const output = new Uint8ClampedArray(data);
const factor = (259 * (adjustment + 255)) / (255 * (259 - adjustment));
for (let i = 0; i < data.length; i += 4) {
output[i] = Math.min(255, Math.max(0, factor * (data[i] - 128) + 128));
output[i + 1] = Math.min(
255,
Math.max(0, factor * (data[i + 1] - 128) + 128)
);
output[i + 2] = Math.min(
255,
Math.max(0, factor * (data[i + 2] - 128) + 128)
);
}
return output;
},
};
self.onmessage = function (e) {
const { filter, imageData, params } = e.data;
if (filters[filter]) {
const result = filters[filter](imageData, params);
self.postMessage(
{
filter,
result: result.buffer,
},
[result.buffer]
);
}
};
// Main thread image processor
class ImageFilterProcessor {
constructor() {
this.worker = new Worker('image-filter-worker.js');
}
applyFilter(canvas, filter, params) {
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
return new Promise((resolve) => {
this.worker.onmessage = (e) => {
const { result } = e.data;
const filteredData = new ImageData(
new Uint8ClampedArray(result),
canvas.width,
canvas.height
);
ctx.putImageData(filteredData, 0, 0);
resolve();
};
this.worker.postMessage(
{
filter,
imageData: {
data: imageData.data.buffer,
width: canvas.width,
height: canvas.height,
},
params,
},
[imageData.data.buffer]
);
});
}
}
Data Processing
// data-processor-worker.js
const processors = {
sort(data, key, order = 'asc') {
return data.sort((a, b) => {
const aVal = a[key];
const bVal = b[key];
if (order === 'asc') {
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
} else {
return aVal > bVal ? -1 : aVal < bVal ? 1 : 0;
}
});
},
filter(data, conditions) {
return data.filter((item) => {
return conditions.every((condition) => {
const { field, operator, value } = condition;
const itemValue = item[field];
switch (operator) {
case '=':
return itemValue === value;
case '!=':
return itemValue !== value;
case '>':
return itemValue > value;
case '<':
return itemValue < value;
case '>=':
return itemValue >= value;
case '<=':
return itemValue <= value;
case 'contains':
return String(itemValue).includes(value);
case 'startsWith':
return String(itemValue).startsWith(value);
case 'endsWith':
return String(itemValue).endsWith(value);
default:
return true;
}
});
});
},
aggregate(data, groupBy, aggregations) {
const groups = {};
// Group data
data.forEach((item) => {
const key = item[groupBy];
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(item);
});
// Apply aggregations
const results = [];
for (const [key, group] of Object.entries(groups)) {
const result = { [groupBy]: key };
aggregations.forEach((agg) => {
const { field, operation, alias } = agg;
const values = group
.map((item) => item[field])
.filter((v) => v != null);
switch (operation) {
case 'sum':
result[alias] = values.reduce((sum, val) => sum + val, 0);
break;
case 'avg':
result[alias] =
values.reduce((sum, val) => sum + val, 0) / values.length;
break;
case 'min':
result[alias] = Math.min(...values);
break;
case 'max':
result[alias] = Math.max(...values);
break;
case 'count':
result[alias] = values.length;
break;
}
});
results.push(result);
}
return results;
},
analyze(data) {
const analysis = {
totalRecords: data.length,
fields: {},
summary: {},
};
if (data.length === 0) return analysis;
// Analyze each field
const fields = Object.keys(data[0]);
fields.forEach((field) => {
const values = data.map((item) => item[field]);
const fieldAnalysis = {
type: typeof values[0],
uniqueValues: new Set(values).size,
nullCount: values.filter((v) => v == null).length,
};
if (fieldAnalysis.type === 'number') {
const numbers = values.filter((v) => typeof v === 'number');
fieldAnalysis.min = Math.min(...numbers);
fieldAnalysis.max = Math.max(...numbers);
fieldAnalysis.avg =
numbers.reduce((sum, n) => sum + n, 0) / numbers.length;
fieldAnalysis.sum = numbers.reduce((sum, n) => sum + n, 0);
} else if (fieldAnalysis.type === 'string') {
fieldAnalysis.minLength = Math.min(
...values.map((v) => (v ? v.length : 0))
);
fieldAnalysis.maxLength = Math.max(
...values.map((v) => (v ? v.length : 0))
);
}
analysis.fields[field] = fieldAnalysis;
});
return analysis;
},
};
self.onmessage = function (e) {
const { operation, data, params } = e.data;
try {
const result = processors[operation](data, ...params);
self.postMessage({ success: true, result });
} catch (error) {
self.postMessage({
success: false,
error: error.message,
});
}
};
Cryptography Operations
// crypto-worker.js
self.onmessage = async function (e) {
const { operation, data } = e.data;
try {
let result;
switch (operation) {
case 'generateKeyPair':
result = await generateKeyPair();
break;
case 'encrypt':
result = await encryptData(data.text, data.publicKey);
break;
case 'decrypt':
result = await decryptData(data.encrypted, data.privateKey);
break;
case 'hash':
result = await hashData(data.text, data.algorithm);
break;
case 'sign':
result = await signData(data.text, data.privateKey);
break;
case 'verify':
result = await verifySignature(
data.text,
data.signature,
data.publicKey
);
break;
}
self.postMessage({ success: true, result });
} catch (error) {
self.postMessage({ success: false, error: error.message });
}
};
async function generateKeyPair() {
const keyPair = await crypto.subtle.generateKey(
{
name: 'RSA-OAEP',
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256',
},
true,
['encrypt', 'decrypt']
);
const publicKey = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
const privateKey = await crypto.subtle.exportKey('jwk', keyPair.privateKey);
return { publicKey, privateKey };
}
async function encryptData(text, publicKeyJwk) {
const encoder = new TextEncoder();
const data = encoder.encode(text);
const publicKey = await crypto.subtle.importKey(
'jwk',
publicKeyJwk,
{
name: 'RSA-OAEP',
hash: 'SHA-256',
},
false,
['encrypt']
);
const encrypted = await crypto.subtle.encrypt(
{ name: 'RSA-OAEP' },
publicKey,
data
);
return Array.from(new Uint8Array(encrypted));
}
async function decryptData(encryptedArray, privateKeyJwk) {
const encrypted = new Uint8Array(encryptedArray).buffer;
const privateKey = await crypto.subtle.importKey(
'jwk',
privateKeyJwk,
{
name: 'RSA-OAEP',
hash: 'SHA-256',
},
false,
['decrypt']
);
const decrypted = await crypto.subtle.decrypt(
{ name: 'RSA-OAEP' },
privateKey,
encrypted
);
const decoder = new TextDecoder();
return decoder.decode(decrypted);
}
async function hashData(text, algorithm = 'SHA-256') {
const encoder = new TextEncoder();
const data = encoder.encode(text);
const hashBuffer = await crypto.subtle.digest(algorithm, data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
}
Performance Optimization
Optimizing Worker Communication
class OptimizedWorkerCommunication {
constructor(workerPath) {
this.worker = new Worker(workerPath);
this.messageQueue = [];
this.isProcessing = false;
this.batchSize = 100;
this.batchTimeout = 16; // One frame
this.setupMessageHandler();
}
setupMessageHandler() {
this.worker.onmessage = (e) => {
const { batchId, results } = e.data;
const batch = this.batches.get(batchId);
if (batch) {
batch.resolve(results);
this.batches.delete(batchId);
}
this.isProcessing = false;
this.processBatch();
};
}
send(data) {
return new Promise((resolve) => {
this.messageQueue.push({ data, resolve });
if (!this.batchTimer) {
this.batchTimer = setTimeout(() => {
this.processBatch();
}, this.batchTimeout);
}
});
}
processBatch() {
if (this.isProcessing || this.messageQueue.length === 0) {
return;
}
clearTimeout(this.batchTimer);
this.batchTimer = null;
const batch = this.messageQueue.splice(0, this.batchSize);
const batchId = Date.now() + Math.random();
this.batches = this.batches || new Map();
this.batches.set(batchId, {
resolve: (results) => {
results.forEach((result, index) => {
batch[index].resolve(result);
});
},
});
this.isProcessing = true;
this.worker.postMessage({
batchId,
items: batch.map((item) => item.data),
});
}
}
// Memory-efficient data streaming
class StreamingWorker {
constructor(workerPath) {
this.worker = new Worker(workerPath);
this.chunkSize = 1024 * 1024; // 1MB chunks
}
async processLargeData(data) {
const chunks = this.createChunks(data);
const results = [];
for (let i = 0; i < chunks.length; i++) {
const result = await this.processChunk(chunks[i], i, chunks.length);
results.push(result);
// Report progress
this.onProgress?.({
current: i + 1,
total: chunks.length,
percentage: ((i + 1) / chunks.length) * 100,
});
}
return this.combineResults(results);
}
createChunks(data) {
const chunks = [];
const dataLength = data.length;
for (let i = 0; i < dataLength; i += this.chunkSize) {
chunks.push(data.slice(i, i + this.chunkSize));
}
return chunks;
}
processChunk(chunk, index, total) {
return new Promise((resolve) => {
this.worker.onmessage = (e) => resolve(e.data);
this.worker.postMessage({
chunk,
chunkIndex: index,
totalChunks: total,
});
});
}
combineResults(results) {
// Combine based on data type
if (results[0] instanceof Array) {
return results.flat();
} else if (typeof results[0] === 'string') {
return results.join('');
} else {
return results;
}
}
}
Error Handling and Debugging
// Enhanced worker with error handling
class RobustWorker {
constructor(workerPath, options = {}) {
this.workerPath = workerPath;
this.options = {
maxRetries: 3,
retryDelay: 1000,
timeout: 30000,
...options,
};
this.initWorker();
}
initWorker() {
this.worker = new Worker(this.workerPath);
this.setupErrorHandling();
this.setupHeartbeat();
}
setupErrorHandling() {
this.worker.onerror = (error) => {
console.error('Worker error:', error);
this.handleError(error);
};
this.worker.onmessageerror = (error) => {
console.error('Worker message error:', error);
this.handleError(error);
};
}
setupHeartbeat() {
this.heartbeatInterval = setInterval(() => {
this.sendHeartbeat();
}, 5000);
this.lastHeartbeat = Date.now();
}
sendHeartbeat() {
const timeout = setTimeout(() => {
console.error('Worker heartbeat timeout');
this.restart();
}, 3000);
this.worker.postMessage({ type: 'heartbeat' });
const messageHandler = (e) => {
if (e.data.type === 'heartbeat-response') {
clearTimeout(timeout);
this.lastHeartbeat = Date.now();
this.worker.removeEventListener('message', messageHandler);
}
};
this.worker.addEventListener('message', messageHandler);
}
handleError(error) {
this.errorCount = (this.errorCount || 0) + 1;
if (this.errorCount <= this.options.maxRetries) {
console.log(`Retrying worker (attempt ${this.errorCount})...`);
setTimeout(() => this.restart(), this.options.retryDelay);
} else {
console.error('Worker failed after max retries');
this.onFatalError?.(error);
}
}
restart() {
this.cleanup();
this.errorCount = 0;
this.initWorker();
this.onRestart?.();
}
cleanup() {
clearInterval(this.heartbeatInterval);
this.worker.terminate();
}
postMessage(data) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Worker timeout'));
}, this.options.timeout);
const messageHandler = (e) => {
if (e.data.id === data.id) {
clearTimeout(timeout);
this.worker.removeEventListener('message', messageHandler);
if (e.data.error) {
reject(new Error(e.data.error));
} else {
resolve(e.data.result);
}
}
};
this.worker.addEventListener('message', messageHandler);
this.worker.postMessage(data);
});
}
}
// Worker-side error handling
self.addEventListener('message', async function (e) {
const { id, type, data } = e.data;
try {
if (type === 'heartbeat') {
self.postMessage({ type: 'heartbeat-response' });
return;
}
const result = await processData(data);
self.postMessage({
id,
result,
});
} catch (error) {
self.postMessage({
id,
error: error.message,
stack: error.stack,
});
}
});
// Debug utilities
class WorkerDebugger {
static enableLogging(worker) {
const originalPostMessage = worker.postMessage.bind(worker);
worker.postMessage = function (data) {
console.log('[Worker Send]', data);
originalPostMessage(data);
};
const originalOnMessage = worker.onmessage;
worker.onmessage = function (e) {
console.log('[Worker Receive]', e.data);
if (originalOnMessage) {
originalOnMessage.call(this, e);
}
};
}
static profileWorker(worker, operation) {
const startTime = performance.now();
const startMemory = performance.memory?.usedJSHeapSize;
return new Promise((resolve) => {
worker.onmessage = (e) => {
const endTime = performance.now();
const endMemory = performance.memory?.usedJSHeapSize;
resolve({
result: e.data,
duration: endTime - startTime,
memoryUsed: endMemory - startMemory,
});
};
worker.postMessage(operation);
});
}
}
Best Practices
-
Choose the right worker type
// Dedicated worker for isolated tasks const dedicated = new Worker('dedicated-task.js'); // Shared worker for cross-tab communication const shared = new SharedWorker('shared-state.js');
-
Optimize data transfer
// Use transferable objects for large data const buffer = new ArrayBuffer(1024 * 1024); worker.postMessage({ buffer }, [buffer]);
-
Implement proper error handling
worker.onerror = (error) => { console.error('Worker error:', error); // Implement recovery strategy };
-
Clean up workers
// Terminate when done worker.terminate(); // Or from inside worker self.close();
Conclusion
Web Workers are powerful for:
- CPU-intensive tasks without blocking the UI
- Parallel processing for better performance
- Background operations like data processing
- Real-time calculations and updates
- Improved user experience with responsive interfaces
Key takeaways:
- Use workers for heavy computations
- Optimize data transfer with transferable objects
- Implement worker pools for concurrent tasks
- Handle errors and implement recovery strategies
- Profile and monitor worker performance
- Clean up resources properly
Master Web Workers to build high-performance JavaScript applications!