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