Heavy JavaScript computation freezing your webpage? Web Workers run code in a separate thread, keeping your UI responsive.
The UI Freezing Problem:
// ❌ BAD: Blocks UI for 5 seconds
function processLargeData() {
const data = Array.from({ length: 10000000 }, (_, i) => i);
const result = data
.filter(n => n % 2 === 0)
.map(n => n * 2)
.reduce((sum, n) => sum + n, 0);
console.log('Result:', result);
}
button.addEventListener('click', processLargeData);
// User clicks button → UI freezes → can't scroll, click, or type
// Page appears broken for 5 seconds
Web Worker Solution:
// Create worker.js file:
// worker.js
self.addEventListener('message', (e) => {
const data = e.data;
// Heavy computation runs in background thread
const result = data
.filter(n => n % 2 === 0)
.map(n => n * 2)
.reduce((sum, n) => sum + n, 0);
// Send result back to main thread
self.postMessage(result);
});
// Main thread (index.html):
const worker = new Worker('worker.js');
worker.addEventListener('message', (e) => {
console.log('Result from worker:', e.data);
updateUI(e.data); // Update UI with result
});
button.addEventListener('click', () => {
const data = Array.from({ length: 10000000 }, (_, i) => i);
worker.postMessage(data); // Send data to worker
// UI stays responsive! User can still interact with page
});
How Web Workers Actually Work:
// Main thread and worker thread run in parallel: // Main Thread Worker Thread // ↓ ↓ // Create worker (starts running worker.js) // ↓ ↓ // postMessage(data) →→→→→→→→→→→→→ receives data // ↓ ↓ // UI stays responsive processes data (heavy work) // User can scroll, click ↓ // ↓ ↓ // ←←←←←←←←←←←←←←←←←←←←← postMessage(result) // receives result ↓ // ↓ continues listening // updateUI(result) // ↓ // Done // Key point: Main thread never blocks!
Worker Limitations:
// ❌ Workers CANNOT access:
// - DOM (document, window)
// - localStorage, sessionStorage
// - Parent page variables directly
// ✅ Workers CAN access:
// - fetch() for network requests
// - setTimeout, setInterval
// - Import other scripts
// - IndexedDB for storage
// - Math, Date, JSON, etc.
// Example of what you can't do:
// worker.js
self.addEventListener('message', () => {
document.getElementById('result').textContent = 'Done'; // ❌ ERROR!
// Workers have no access to DOM
});
// Instead, send message to main thread:
// worker.js
self.addEventListener('message', () => {
self.postMessage({ type: 'done', message: 'Processing complete' });
});
// main.js
worker.addEventListener('message', (e) => {
if (e.data.type === 'done') {
document.getElementById('result').textContent = e.data.message; // ✅ Works
}
});
Transferable Objects (Zero-Copy):
// By default, postMessage copies data const largeArray = new Uint8Array(10000000); // 10MB array worker.postMessage(largeArray); // Copies 10MB → main thread still has array // Worker gets copy → uses 20MB total RAM // ✅ Use Transferable for zero-copy worker.postMessage(largeArray, [largeArray.buffer]); // Transfers ownership → main thread loses array // Worker gets array → uses only 10MB RAM // 50% memory savings! // After transfer: console.log(largeArray.byteLength); // 0 (empty, transferred away) // Transferable types: // - ArrayBuffer // - MessagePort // - ImageBitmap // - OffscreenCanvas
Shared Workers (Multiple Tabs):
// Regular Worker: Each tab gets its own instance
const worker = new Worker('worker.js'); // Tab 1 has separate worker
const worker = new Worker('worker.js'); // Tab 2 has separate worker
// Shared Worker: All tabs share single instance
const worker = new SharedWorker('shared-worker.js');
// shared-worker.js
self.addEventListener('connect', (e) => {
const port = e.ports[0];
port.addEventListener('message', (msg) => {
// Broadcast to all connected tabs
self.ports.forEach(p => p.postMessage(msg.data));
});
port.start();
});
// Use case: Real-time chat shared across multiple tabs
// One WebSocket connection in Shared Worker
// All tabs receive messages
Real-World Example – Image Processing:
// image-worker.js
self.addEventListener('message', (e) => {
const { imageData, filter } = e.data;
const data = imageData.data;
if (filter === 'grayscale') {
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
}
} else if (filter === 'invert') {
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // R
data[i + 1] = 255 - data[i + 1]; // G
data[i + 2] = 255 - data[i + 2]; // B
}
}
self.postMessage({ imageData }, [imageData.data.buffer]);
});
// main.js
const worker = new Worker('image-worker.js');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
document.getElementById('grayscale-btn').addEventListener('click', () => {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
worker.postMessage(
{ imageData, filter: 'grayscale' },
[imageData.data.buffer] // Transfer for performance
);
});
worker.addEventListener('message', (e) => {
const { imageData } = e.data;
ctx.putImageData(imageData, 0, 0);
// UI stayed responsive during processing!
});
Error Handling:
worker.addEventListener('error', (e) => {
console.error('Worker error:', e.message);
console.error('File:', e.filename);
console.error('Line:', e.lineno);
});
// In worker, unhandled errors bubble to main thread
// worker.js
self.addEventListener('message', () => {
throw new Error('Something went wrong!');
// Main thread's error listener catches this
});
Terminate Worker:
// From main thread worker.terminate(); // Immediately stops worker // From worker (self-termination) self.close(); // Worker stops itself
