Skip to main content

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 CaseRecommendation
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