Web Workers and Multithreading
Answer
Web Workers allow JavaScript to run scripts in background threads, enabling true parallel execution without blocking the main UI thread.
Worker Types
Creating a Dedicated Worker
// main.js
const worker = new Worker("worker.js");
// Send message to worker
worker.postMessage({ type: "compute", data: [1, 2, 3, 4, 5] });
// Receive message from worker
worker.onmessage = (event) => {
console.log("Result:", event.data);
};
// Handle errors
worker.onerror = (error) => {
console.error("Worker error:", error.message);
};
// Terminate worker
worker.terminate();
// worker.js
self.onmessage = (event) => {
const { type, data } = event.data;
if (type === "compute") {
// Heavy computation won't block UI
const result = data.reduce((sum, n) => sum + n, 0);
// Send result back
self.postMessage(result);
}
};
Inline Worker (No Separate File)
const workerCode = `
self.onmessage = (e) => {
const result = e.data.map(n => n * 2);
self.postMessage(result);
};
`;
const blob = new Blob([workerCode], { type: "application/javascript" });
const worker = new Worker(URL.createObjectURL(blob));
worker.postMessage([1, 2, 3, 4, 5]);
worker.onmessage = (e) => console.log("Doubled:", e.data);
Transferable Objects
// Regular postMessage (data is copied)
const largeArray = new Float64Array(1000000);
worker.postMessage({ data: largeArray });
// largeArray is still usable here
// Transferable (data is moved, not copied - faster!)
const buffer = new ArrayBuffer(1000000);
worker.postMessage({ buffer }, [buffer]);
// buffer is now neutered (unusable in main thread)
// Example with ImageData
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const imageData = ctx.getImageData(0, 0, 800, 600);
worker.postMessage({ imageData }, [imageData.data.buffer]);
Practical Example: Image Processing
// main.js
const worker = new Worker("image-worker.js");
async function processImage(file) {
const bitmap = await createImageBitmap(file);
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const ctx = canvas.getContext("2d");
ctx.drawImage(bitmap, 0, 0);
const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
worker.postMessage(
{
type: "grayscale",
imageData,
},
[imageData.data.buffer]
);
}
worker.onmessage = (e) => {
const processedImageData = e.data;
// Display processed image
ctx.putImageData(processedImageData, 0, 0);
};
// image-worker.js
self.onmessage = (e) => {
const { type, imageData } = e.data;
if (type === "grayscale") {
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; // R
data[i + 1] = avg; // G
data[i + 2] = avg; // B
// data[i + 3] is Alpha, unchanged
}
self.postMessage(imageData, [imageData.data.buffer]);
}
};
Worker Limitations
// ❌ Workers CANNOT access:
document; // No DOM
window; // No window object
parent; // No parent window
localStorage; // No localStorage
// ✅ Workers CAN access:
navigator; // Navigator object
location; // Read-only location
XMLHttpRequest / fetch; // Network requests
WebSockets; // WebSocket connections
IndexedDB; // IndexedDB database
importScripts(); // Import other scripts
setTimeout / setInterval; // Timers
Shared Workers
// shared-worker.js
const connections = [];
self.onconnect = (e) => {
const port = e.ports[0];
connections.push(port);
port.onmessage = (event) => {
// Broadcast to all connections
connections.forEach((conn) => {
conn.postMessage(event.data);
});
};
port.start();
};
// page1.js and page2.js
const worker = new SharedWorker("shared-worker.js");
worker.port.onmessage = (e) => {
console.log("Received:", e.data);
};
worker.port.postMessage("Hello from page!");
worker.port.start();
Worker Pool Pattern
class WorkerPool {
constructor(workerScript, poolSize = navigator.hardwareConcurrency) {
this.workers = [];
this.queue = [];
this.activeWorkers = 0;
for (let i = 0; i < poolSize; i++) {
const worker = new Worker(workerScript);
worker.busy = false;
this.workers.push(worker);
}
}
execute(data) {
return new Promise((resolve, reject) => {
const task = { data, resolve, reject };
const availableWorker = this.workers.find((w) => !w.busy);
if (availableWorker) {
this.runTask(availableWorker, task);
} else {
this.queue.push(task);
}
});
}
runTask(worker, task) {
worker.busy = true;
worker.onmessage = (e) => {
task.resolve(e.data);
worker.busy = false;
if (this.queue.length > 0) {
this.runTask(worker, this.queue.shift());
}
};
worker.onerror = (e) => {
task.reject(e);
worker.busy = false;
};
worker.postMessage(task.data);
}
}
// Usage
const pool = new WorkerPool("compute-worker.js", 4);
const results = await Promise.all([
pool.execute({ task: "heavy1" }),
pool.execute({ task: "heavy2" }),
pool.execute({ task: "heavy3" }),
pool.execute({ task: "heavy4" }),
]);
When to Use Workers
| Use Case | Recommendation |
|---|---|
| Heavy computations | ✅ Yes |
| Image/video processing | ✅ Yes |
| Data parsing (large JSON) | ✅ Yes |
| Simple calculations | ❌ Overhead not worth it |
| DOM manipulation | ❌ Not possible |
| Animation | ❌ Use requestAnimationFrame |
Key Points
- Workers run in separate threads
- Communication via postMessage/onmessage
- Use Transferable objects for large data
- Workers cannot access DOM
- Use worker pools for multiple tasks
- Consider overhead for small tasks