Web Workers vs Shared Workers vs Service Workers
A Practical, Mental-Model-First Guide for Modern Web Apps
Modern web applications must be fast, responsive, resilient, and multi-tab aware.
To achieve this, browsers provide three different background execution models:
- Web Workers
- Shared Workers
- Service Workers
Although they sound similar, they solve very different problems.
This blog builds a clear mental model, explains when to use which, dives into capabilities & limitations, and walks through real-world usage with code examples.
Table of Contents
- Why Background Workers Exist
- Quick Mental Model
- Dedicated Web Worker
- Shared Worker
- Service Worker
- Comparison Table (Web vs Shared vs Service Worker)
- Choosing the Right Worker (Decision Guide)
- Real World Usage Scenarios
- Trade offs & Gotchas
- Security & Compliance Considerations
- Final Recommendations
Why Background Workers Exist
JavaScript runs on a single-threaded event loop.
If you:
- parse a large file
- encrypt data
- process images
- handle streaming data
- coordinate across multiple tabs
β¦on the main thread, your UI freezes.
Workers exist to move work off the main thread while keeping your app responsive.
Quick Mental Model
Think of workers like this:
π§ Mental Model
Web Worker (Dedicated Worker)
β A background thread for one page only
β Best for CPU-heavy tasksShared Worker
β A background thread shared by multiple tabs
β Best for centralized state or shared resourcesService Worker
β A network proxy + offline engine
β Best for caching, offline, push, background sync
β οΈ Service Workers are not for long-running compute loops
Dedicated Web Worker
What is a Web Worker?
A Dedicated Web Worker:
- Runs in a separate thread
- Is tied to one page/tab
- Has no DOM or UI access
- Communicates via
postMessage
When to Use Web Workers
Use a Web Worker when:
- One page needs heavy computation
- You want to keep UI smooth
- No cross-tab coordination is needed
Common Use Cases
- JSON parsing
- Image/video processing
- Cryptography
- Large data transformations
- Search indexing
Architecture
Main Thread (UI)
|
| postMessage
v
Dedicated Worker
(CPU-heavy work)
Example: Offloading Heavy Compute
worker.js
self.onmessage = (e) => {
const { type, payload } = e.data || {};
if (type === 'sum') {
const { array } = payload || {};
const total = array.reduce((a, b) => a + b, 0);
postMessage({ type: 'sum:result', total });
}
};
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] }
});
// worker.terminate(); // cleanup when done
Shared Worker
What is a Shared Worker?
A SharedWorker:
- Is shared by multiple tabs/windows
- Belongs to the same origin
- Maintains in-memory shared state
- Uses MessagePort instead of
onmessage
Why Shared Workers Exist
Without Shared Workers:
- Each tab opens its own WebSocket
- Each tab maintains duplicate state
- Backend load increases
Shared Workers solve this by acting as a single coordinator.
When to Use Shared Workers
Use a SharedWorker when:
- Multiple tabs need shared state
- You want one WebSocket per user
- You need cross-tab coordination
Architecture
Tab A ββ
ββ SharedWorker ββ WebSocket
Tab B ββ
Example: Single WebSocket Shared Across Tabs
shared-worker.js
const ports = new Set();
let socket;
function broadcast(data, exceptPort) {
for (const port of ports) {
if (port !== exceptPort) {
port.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' });
}
onconnect = (e) => {
const port = e.ports[0];
ports.add(port);
ensureSocket();
port.onmessage = (evt) => {
if (evt.data?.type === 'ws:send') {
socket?.send(JSON.stringify(evt.data.payload));
}
};
port.start();
port.postMessage({ type: 'connected' });
port.onclose = () => {
ports.delete(port);
if (ports.size === 0) socket?.close();
};
};
In Each Page
const worker = new SharedWorker('/shared-worker.js', { name: 'shared' });
worker.port.start();
worker.port.onmessage = (e) => {
console.log('From shared worker:', e.data);
};
worker.port.postMessage({
type: 'ws:send',
payload: { message: 'hello' }
});
Service Worker
What is a Service Worker?
A Service Worker:
- Runs outside any page
- Acts as a network proxy
- Is event-driven
- Enables offline, caching, push, sync
Why Service Workers Are Different
They:
- Intercept
fetchrequests - Control multiple pages
- Can outlive page reloads
- Are not always running
Lifecycle
Install β Activate β Fetch / Push / Sync β Terminate
Service workers wake up only for events
Example: Offline Caching + Request Routing
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();
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(
keys.map(k => k !== CACHE_NAME && caches.delete(k))
)
)
);
self.clients.claim();
});
self.addEventListener('fetch', (event) => {
if (event.request.method !== 'GET') return;
event.respondWith(
caches.match(event.request).then(cached =>
cached || fetch(event.request)
)
);
});
Registering from the Page
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW registered', reg.scope))
.catch(console.error);
}
Comparison Table
| Feature | Web Worker | Shared Worker | Service Worker |
|---|---|---|---|
| Scope | One page | Multiple tabs | Entire origin |
| Lifetime | While referenced | While ports exist | Event-driven |
| DOM Access | β | β | β |
| CPU Work | β | β | β |
| Fetch Interception | β | β | β |
| Offline Support | β | β | β |
| Push Notifications | β | β | β |
Choosing the Right Worker
Use Web Worker if:
- Heavy computation per page
- No shared state needed
Use Shared Worker if:
- Multiple tabs share state
- One WebSocket per user
- Cross-tab coordination
Use Service Worker if:
- Offline-first app
- Network caching
- Push notifications
- Background sync
Real-World Usage Scenarios
- Google Docs β SharedWorker for sync + Service Worker for offline
- Figma β Web Workers for rendering & computation
- E-commerce PWA β Service Worker for caching & resilience
- Trading dashboards β SharedWorker for real-time streams
Trade-offs & Gotchas
Web Worker
- β Simple
- β No sharing across tabs
Shared Worker
- β Efficient resource sharing
- β Lifecycle complexity
- β Limited support in private browsing
Service Worker
- β Extremely powerful
- β Complex lifecycle
- β Careful cache invalidation required
Security & Compliance Tips
- Never store secrets in workers
- Validate all incoming messages
- Avoid caching authenticated responses
- Always use HTTPS
- Follow least-privilege principles
For enterprise apps (e.g., Oracle ecosystems), align with internal security and compliance guidelines before adoption.
Final Recommendations
Think in responsibilities, not APIs:
- Compute β Web Worker
- Coordination β Shared Worker
- Network & Offline β Service Worker
If you share your specific use case, I can help you design:
- Messaging patterns
- Caching strategies
- Worker composition (yes, they can coexist!)
More Details:
Get all articles related to system design
Hastag: SystemDesignWithZeeshanAli
Git: https://github.com/ZeeshanAli-0704/front-end-system-design
Top comments (0)