š·
Advanced
12 min readWeb Workers & Multithreading
Offloading heavy computation to background threads
Understanding Web Workers
Web Workers allow you to run JavaScript in background threads, preventing long-running scripts from blocking the UI thread. This is essential for CPU-intensive operations.
Basic Web Worker Setup
// Main thread (main.js)
// ā Bad: CPU-intensive task blocks UI
function processLargeDataset(data) {
// Complex calculations that freeze the UI
return data.map(item => expensiveOperation(item));
}
// ā
Good: Use Web Worker for heavy computation
const worker = new Worker('worker.js');
// Send data to worker
worker.postMessage({ data: largeDataset, operation: 'process' });
// Receive results from worker
worker.onmessage = (event) => {
const results = event.data;
displayResults(results);
};
// Handle errors
worker.onerror = (error) => {
console.error('Worker error:', error.message);
};
// Terminate worker when done
// worker.terminate();
// Worker thread (worker.js)
// Listen for messages from main thread
self.onmessage = (event) => {
const { data, operation } = event.data;
let result;
switch (operation) {
case 'process':
result = processData(data);
break;
case 'calculate':
result = performCalculation(data);
break;
default:
result = { error: 'Unknown operation' };
}
// Send result back to main thread
self.postMessage(result);
};
function processData(data) {
// CPU-intensive operations
return data.map(item => {
// Complex transformations
return expensiveOperation(item);
});
}
Transferable Objects
Transfer ownership of data instead of copying for better performance:
// ā Bad: Data is copied (slow for large arrays)
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
worker.postMessage({ buffer: buffer });
// buffer is still accessible here (copied)
// ā
Good: Transfer ownership (zero-copy)
const buffer = new ArrayBuffer(1024 * 1024);
worker.postMessage({ buffer: buffer }, [buffer]);
// buffer is no longer accessible here (transferred)
// Example with image processing
async function processImage(imageData) {
const worker = new Worker('image-worker.js');
return new Promise((resolve, reject) => {
worker.onmessage = (e) => {
resolve(e.data.processedImage);
worker.terminate();
};
worker.onerror = reject;
// Transfer ImageData buffer
worker.postMessage({ imageData }, [imageData.data.buffer]);
});
}
Shared Workers
Share a single worker instance across multiple browser contexts:
// Create shared worker
const sharedWorker = new SharedWorker('shared-worker.js');
// Communicate via port
sharedWorker.port.start();
sharedWorker.port.postMessage({ type: 'init', userId: 123 });
sharedWorker.port.onmessage = (event) => {
console.log('Message from shared worker:', event.data);
};
// shared-worker.js
const connections = new Set();
self.onconnect = (event) => {
const port = event.ports[0];
connections.add(port);
port.onmessage = (e) => {
// Broadcast to all connections
connections.forEach(conn => {
if (conn !== port) {
conn.postMessage(e.data);
}
});
};
port.start();
};
Worker Pool Pattern
class WorkerPool {
constructor(workerScript, poolSize = 4) {
this.workerScript = workerScript;
this.poolSize = poolSize;
this.workers = [];
this.taskQueue = [];
// Initialize worker pool
for (let i = 0; i < poolSize; i++) {
this.workers.push({
worker: new Worker(workerScript),
busy: false
});
}
}
async execute(data) {
return new Promise((resolve, reject) => {
const task = { data, resolve, reject };
// Try to assign to available worker
const availableWorker = this.workers.find(w => !w.busy);
if (availableWorker) {
this.runTask(availableWorker, task);
} else {
// Queue task if all workers busy
this.taskQueue.push(task);
}
});
}
runTask(workerObj, task) {
workerObj.busy = true;
const handleMessage = (event) => {
workerObj.busy = false;
workerObj.worker.removeEventListener('message', handleMessage);
task.resolve(event.data);
// Process next task in queue
if (this.taskQueue.length > 0) {
const nextTask = this.taskQueue.shift();
this.runTask(workerObj, nextTask);
}
};
const handleError = (error) => {
workerObj.busy = false;
workerObj.worker.removeEventListener('error', handleError);
task.reject(error);
};
workerObj.worker.addEventListener('message', handleMessage);
workerObj.worker.addEventListener('error', handleError);
workerObj.worker.postMessage(task.data);
}
terminate() {
this.workers.forEach(w => w.worker.terminate());
this.workers = [];
this.taskQueue = [];
}
}
// Usage
const pool = new WorkerPool('computation-worker.js', 4);
// Process multiple tasks in parallel
const promises = largeDataset.map(chunk =>
pool.execute({ operation: 'process', data: chunk })
);
const results = await Promise.all(promises);
pool.terminate();
Real-World Use Cases
// 1. Image Processing
// image-worker.js
self.onmessage = (e) => {
const { imageData, filter } = e.data;
const filtered = applyFilter(imageData, filter);
self.postMessage({ imageData: filtered }, [filtered.data.buffer]);
};
// 2. Data Parsing
// parser-worker.js
self.onmessage = (e) => {
const { csv } = e.data;
const parsed = parseCSV(csv); // CPU-intensive parsing
self.postMessage({ data: parsed });
};
// 3. Cryptography
// crypto-worker.js
importScripts('crypto-lib.js');
self.onmessage = async (e) => {
const { data, operation } = e.data;
if (operation === 'encrypt') {
const encrypted = await encrypt(data);
self.postMessage({ encrypted });
} else if (operation === 'decrypt') {
const decrypted = await decrypt(data);
self.postMessage({ decrypted });
}
};
// 4. Real-time Data Processing
// analytics-worker.js
let dataBuffer = [];
self.onmessage = (e) => {
dataBuffer.push(e.data);
if (dataBuffer.length >= 1000) {
const insights = analyzeData(dataBuffer);
self.postMessage({ insights });
dataBuffer = [];
}
};
Worker Limitations
// Workers cannot access:
// - DOM (document, window)
// - Parent page objects
// - localStorage/sessionStorage (use IndexedDB instead)
// Workers CAN access:
// - setTimeout, setInterval
// - fetch, XMLHttpRequest
// - IndexedDB
// - WebSockets
// - importScripts() to load libraries
// Example: Load external libraries in worker
// worker.js
importScripts(
'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js',
'https://cdn.jsdelivr.net/npm/date-fns@2.29.3/index.min.js'
);
self.onmessage = (e) => {
// Can now use lodash and date-fns
const result = _.groupBy(e.data, 'category');
self.postMessage(result);
};
Best Practices
- Use Web Workers for CPU-intensive tasks (image processing, data parsing, cryptography)
- Transfer ArrayBuffers instead of copying when possible
- Implement worker pools for handling multiple parallel tasks
- Keep worker scripts small - use
importScripts()for libraries - Always terminate workers when no longer needed to free memory
- Handle errors gracefully with
worker.onerror - Use Shared Workers for shared state across tabs
- Consider using
comlinklibrary for easier Worker communication - Don't create workers in loops - reuse existing workers
- Profile to ensure workers provide actual benefit (overhead exists)