Web Workers & Service Workers — Multi-Threaded JavaScript Guide
In this tutorial, you'll learn about Web Workers & Service Workers. We cover key concepts, practical examples, and best practices.
Web Workers run JavaScript in background threads, keeping the UI responsive during heavy computation. Service Workers act as programmable network proxies, enabling offline support, caching, push notifications, and background sync.
In this tutorial, you'll learn to use Web Workers for CPU-intensive tasks without freezing the UI, and Service Workers for offline-first experiences, asset caching, and push notifications. By the end, you'll build applications that stay responsive under heavy load and work without a network connection.
Real-world use: Durga Antivirus Pro uses a Web Worker to compute file hashes (MD5, SHA256) for threat signature matching without blocking the UI. A Service Worker caches virus definition databases for offline scanning and intercepts network requests to block known malicious domains before they reach the browser.
flowchart TD A[Main Thread] --> B[UI Rendering] A --> C[User Events] A --> D[Web Worker] D --> E[Heavy Computation] D --> F[File Hashing] D --> G[Data Processing] A --> H[Service Worker] H --> I[Cache Assets] H --> J[Intercept Requests] H --> K[Push Notifications] H --> L[Background Sync] style D fill:#4af,color:#fff style H fill:#f90,color:#fff
Web Workers — Dedicated Background Threads
A Web Worker runs a separate JavaScript file in its own thread.
// main.js
const worker = new Worker('hash-worker.js');
worker.postMessage({
file: '/home/docs/report.pdf',
algorithm: 'sha256'
});
worker.onmessage = (event) => {
const { hash, duration } = event.data;
console.log(`SHA256: ${hash}`);
console.log(`Computed in ${duration}ms`);
updateHashDisplay(hash);
};
worker.onerror = (error) => {
console.error('Worker error:', error.message);
};
// hash-worker.js
self.onmessage = async (event) => {
const { file, algorithm } = event.data;
const startTime = performance.now();
try {
const response = await fetch(file);
const buffer = await response.arrayBuffer();
const hashBuffer = await crypto.subtle.digest(algorithm, buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
self.postMessage({
hash: hashHex,
duration: performance.now() - startTime
});
} catch (error) {
self.postMessage({ error: error.message });
}
};
Expected output:
SHA256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Computed in 342ms
Worker Communication Patterns
Workers communicate via postMessage and receive messages via the message event.
// main.js — transferable objects for zero-copy performance
const worker = new Worker('processor.js');
const largeBuffer = new ArrayBuffer(1024 * 1024 * 100);
worker.postMessage({ buffer: largeBuffer }, [largeBuffer]);
// largeBuffer is now empty in the main thread — ownership transferred
// main.js — shared buffer for concurrent access
const sharedBuffer = new SharedArrayBuffer(1024);
const sharedArray = new Int32Array(sharedBuffer);
worker.postMessage({ shared: sharedBuffer });
Terminating Workers
Clean up workers when they're no longer needed.
// From the main thread
worker.terminate();
// From inside the worker
self.close();
Service Workers — Network Proxy
A Service Worker intercepts network requests and enables offline functionality.
// sw.js
const CACHE_NAME = 'dodatech-cache-v1';
const ASSETS = [
'/',
'/styles/main.css',
'/scripts/app.js',
'/offline.html'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(ASSETS);
})
);
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => {
return Promise.all(
keys
.filter((key) => key !== CACHE_NAME)
.map((key) => caches.delete(key))
);
})
);
clients.claim();
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request).then((response) => {
if (response.status === 200) {
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, clone);
});
}
return response;
}).catch(() => {
return caches.match('/offline.html');
});
})
);
});
Registering a Service Worker
// Register from the main page
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', {
scope: '/'
}).then((registration) => {
console.log('SW registered:', registration.scope);
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
showUpdateNotification();
}
});
});
}).catch((error) => {
console.error('SW registration failed:', error);
});
}
Push Notifications
Send push notifications from the server through the Service Worker.
// sw.js — listen for push events
self.addEventListener('push', (event) => {
const data = event.data.json();
const options = {
body: data.body,
icon: '/icon-192.png',
badge: '/badge-72.png',
data: { url: data.url },
actions: [
{ action: 'view', title: 'View Details' },
{ action: 'dismiss', title: 'Dismiss' }
]
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'view') {
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
}
});
Background Sync
Sync data when the network becomes available.
// sw.js
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-threats') {
event.waitUntil(syncThreatReports());
}
});
async function syncThreatReports() {
const db = await openIndexedDB();
const pending = await db.getAll('pending-threats');
for (const report of pending) {
await fetch('/api/threats/report', {
method: 'POST',
body: JSON.stringify(report)
});
await db.delete('pending-threats', report.id);
}
}
// Register a sync from the page
navigator.serviceWorker.ready.then((registration) => {
registration.sync.register('sync-threats');
});
Web Worker vs Service Worker
| Feature | Web Worker | Service Worker |
|---|---|---|
| Purpose | Background computation | Network proxy, caching |
| DOM access | No | No |
| Lifecycle | Page-bound | Independent, persists |
| Terminate | Can be terminated | Browser manages |
| API access | Most Web APIs | Limited (no DOM, no localStorage) |
| Can block page unload | No | Yes (extendable events) |
| Number per page | Unlimited | One per scope |
Common Errors
Service Worker scope too narrow — A Service Worker at
/scripts/sw.jscan only intercept requests under/scripts/. Register it at the root (/sw.js) for full-site coverage.Not handling worker errors — Uncaught errors in Web Workers silently fail. Always add an
onerrorhandler and wrap worker code in try-catch blocks.Using
alert()orconfirm()in workers — Workers cannot access the DOM. This includesalert(),confirm(),localStorage, anddocument. UsepostMessageto communicate back to the main thread.Forgetting to update the cache — Old Service Worker caches accumulate. Use the
activateevent to delete outdated caches by name.Service Worker not updating — The browser waits for all tabs to close before activating a new Service Worker. Use
self.skipWaiting()andclients.claim()for immediate activation.
Practice Questions
What's the difference between a Web Worker and a Service Worker? Web Workers execute scripts in background threads for computation. Service Workers act as programmable network proxies with persistent lifecycle, supporting offline caching, push notifications, and background sync.
Can Web Workers access the DOM? No. Workers run in a separate global context (
selforDedicatedWorkerGlobalScope) with no access todocument,window, or DOM APIs.How do you update a Service Worker? Update the
sw.jsfile. The browser detects the byte difference during the next navigation. The new worker installs, waits inwaitingstate, and activates after all tabs close or whenskipWaiting()is called.What are Transferable Objects and when should you use them? Transferable objects (like
ArrayBuffer) can be passed to workers without copying — ownership moves to the worker. Use them for large data (megabytes+) to avoid duplicating memory.Why does a Service Worker need HTTPS? Service Workers have powerful interception capabilities. HTTPS ensures the Service Worker script hasn't been tampered with during delivery. Localhost is exempt for development.
Challenge
Build a file scanning web app that uses a Web Worker to compute SHA256 hashes of uploaded files without freezing the UI. Show a progress indicator for files larger than 100MB. The Service Worker should cache the hashing library and definition files, and queue scan results for upload when offline using Background Sync.
Real-World Task
Implement the offline scanning architecture for Durga Antivirus Pro web dashboard. Use a Web Worker to compute file hashes for threat signature matching — process 500MB files without UI lag. Use a Service Worker to cache the virus definition database (updated daily via push), intercept API requests to known malicious domains, and queue threat reports with Background Sync when the network is unavailable.
Previous: JavaScript Async/Await | Next: Progressive Web Apps | Related: Web Performance
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro