Quick mental model
- Web Worker (DedicatedWorker): Background thread for one page. No UI, no DOM. Best for CPU-heavy tasks off the main thread.
- SharedWorker: Background thread shared by multiple tabs/windows of the same origin. Great for centralized state or shared resources (e.g., one WebSocket).
- Service Worker: Event-driven background proxy between your app and network. Can intercept fetch, cache, work offline, receive push, do background sync. Not for long-running compute loops.
When to use which
- Use Web Worker when a single page needs heavy compute or streaming processing without freezing the UI.
- Use SharedWorker when multiple tabs need to coordinate or share a single connection/state.
- Use Service Worker when you need offline caching, request routing, push notifications, or background sync.
Key differences
- Scope:
- Web Worker: one page/tab.
- SharedWorker: all pages of same origin (per browser process) that connect.
- Service Worker: origin + path scope (controls all pages under its scope once installed).
- Lifecycle:
- Web/Shared Workers: stay alive while referenced; run continuously.
- Service Worker: event-driven; starts for events (install, activate, fetch, push) and is terminated when idle.
- Capabilities:
- Web/Shared Workers: no fetch interception; can do compute, IndexedDB, postMessage.
- Service Worker: can intercept fetch, use Cache Storage, handle push/sync, message clients.
- Networking:
- Web/Shared Workers: use fetch/WebSocket like a page, but cannot proxy other pages’ requests.
- Service Worker: can proxy and cache page requests and responses.
Code examples
1) Web Worker (Dedicated) – offload heavy compute from the main thread
- worker.js
// worker.js
self.onmessage = (e) => {
const { type, payload } = e.data || {};
if (type === 'sum') {
const { array } = payload || {};
// Example CPU work
const total = array.reduce((a, b) => a + b, 0);
postMessage({ type: 'sum:result', total });
}
};
- main.js
// main.js
const worker = new Worker('/worker.js', { type: 'module' });
worker.onmessage = (e) => {
if (e.data?.type === 'sum:result') {
console.log('Sum is', e.data.total);
}
};
worker.postMessage({ type: 'sum', payload: { array: [1,2,3,4] } });
// Cleanup when done
// worker.terminate();
2) SharedWorker – centralized state across tabs (e.g., single WebSocket)
- shared-worker.js
// shared-worker.js
// One instance is shared by all tabs in the same origin+script URL (per process)
const ports = new Set();
let socket;
function broadcast(data, exceptPort) {
for (const p of ports) {
if (p !== exceptPort) p.postMessage(data);
}
}
function ensureSocket() {
if (socket && socket.readyState === WebSocket.OPEN) return;
socket = new WebSocket('wss://example.com/realtime');
socket.onopen = () => broadcast({ type: 'ws:status', status: 'open' });
socket.onmessage = (evt) => broadcast({ type: 'ws:message', data: evt.data });
socket.onclose = () => broadcast({ type: 'ws:status', status: 'closed' });
socket.onerror = () => broadcast({ type: 'ws:status', status: 'error' });
}
onconnect = (e) => {
const port = e.ports[0];
ports.add(port);
ensureSocket();
port.onmessage = (evt) => {
const msg = evt.data;
if (msg?.type === 'ws:send') {
if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify(msg.payload));
else port.postMessage({ type: 'ws:status', status: 'not-open' });
}
};
port.start();
port.postMessage({ type: 'hello', from: 'shared-worker' });
port.onclose = () => {
ports.delete(port);
if (ports.size === 0) {
// Optionally close the socket when no clients remain
try { socket?.close(); } catch {}
socket = null;
}
};
};
- In each page
// page.js
const sw = new SharedWorker('/shared-worker.js', { name: 'app-shared' });
sw.port.start();
sw.port.onmessage = (e) => {
const { type } = e.data || {};
if (type === 'ws:message') {
console.log('Realtime:', e.data.data);
} else if (type === 'ws:status') {
console.log('Socket status:', e.data.status);
}
};
// Send a message through the single shared WebSocket
function sendRealtime(payload) {
sw.port.postMessage({ type: 'ws:send', payload });
}
3) Service Worker – offline caching, request routing, client messaging
- sw.js (service worker file)
// sw.js
const CACHE_NAME = 'app-cache-v1';
const PRECACHE_URLS = [
'/', '/index.html', '/styles.css', '/app.js'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(PRECACHE_URLS))
);
self.skipWaiting(); // optional
});
self.addEventListener('activate', (event) => {
event.waitUntil((async () => {
const keys = await caches.keys();
await Promise.all(keys.map(k => (k !== CACHE_NAME) && caches.delete(k)));
await self.clients.claim(); // control open pages ASAP
})());
});
// Intercept fetch: stale-while-revalidate for GET requests
self.addEventListener('fetch', (event) => {
const req = event.request;
if (req.method !== 'GET') return; // don't cache non-GET by default
event.respondWith((async () => {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(req);
const networkPromise = fetch(req).then(res => {
// Optionally check res.ok && Content-Type before caching
cache.put(req, res.clone()).catch(() => {});
return res;
}).catch(() => cached); // offline fallback to cache
return cached || networkPromise;
})());
});
// Optional: receive push messages (requires Push API setup)
self.addEventListener('push', (event) => {
const data = event.data?.json() || {};
event.waitUntil(
self.registration.showNotification('Update', { body: data.message || 'New event' })
);
});
// Message from page -> SW -> broadcast to all clients
self.addEventListener('message', async (event) => {
const allClients = await self.clients.matchAll({ includeUncontrolled: true, type: 'window' });
for (const client of allClients) {
client.postMessage({ type: 'sw:broadcast', payload: event.data });
}
});
- Register the Service Worker from a page
if ('serviceWorker' in navigator) {
// Must be served over HTTPS (or localhost)
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then((reg) => console.log('SW registered', reg.scope))
.catch(console.error);
// Listen to messages from the SW
navigator.serviceWorker.addEventListener('message', (e) => {
if (e.data?.type === 'sw:broadcast') {
console.log('From SW:', e.data.payload);
}
});
// Send a message to the SW
async function pingSW() {
const reg = await navigator.serviceWorker.getRegistration();
reg?.active?.postMessage({ type: 'hello-from-page' });
}
}
Choosing guide
- Pick Web Worker if:
- Per-page compute offloading (parsing, crypto, image/video processing).
- You don’t need cross-tab sharing.
- Pick SharedWorker if:
- Multiple tabs must share an in-memory resource (one WebSocket, rate limiter, job queue).
- You need cross-tab coordination but not network interception.
- Pick Service Worker if:
- Offline/PWA, caching strategies (precache, runtime cache), fetch proxying, push notifications, background sync.
- You want to improve performance and resilience regardless of tabs.
Trade-offs and gotchas
- Web Worker:
- Pros: Simple, great for CPU work; off main thread; can use transferable objects.
- Cons: One page only; you manage worker lifetime; cannot intercept other requests.
- SharedWorker:
- Pros: Centralized state across tabs; efficient resource sharing; off main thread.
- Cons: More complex lifecycle; limited in some privacy modes; different process models may create multiple instances; no fetch interception.
- Service Worker:
- Pros: Offline-first, caching, background events (push/sync), request routing.
- Cons: Event-driven (no long loops); lifecycle nuances (install/activate/update); HTTPS required; careful cache invalidation required.
Security and compliance tips
- Never store secrets or regulated data in client storage or broadcast them across workers.
- Validate all messages (schema, types); treat all inbound messages as untrusted.
- For Service Workers, avoid caching authenticated or sensitive responses unless you designed strict scoping and cache controls. Respect Cache-Control and use versioned caches.
- Use HTTPS, Content Security Policy (CSP), and least-privilege approaches.
- If this is for Oracle-related work, ensure alignment with Oracle security, privacy, and compliance guidelines. Verify any third-party libraries against internal policies before adoption.
If you share your use case (compute vs. cross-tab coordination vs. offline/push), I can recommend an exact design and caching/messaging patterns.
Top comments (0)