Web Workers vs Service Workers: Different Tools for Different Problems
Both Web Workers and Service Workers are background JavaScript contexts that run independently of the browser's main thread. Both communicate via message passing. Both are unavailable in older browsers. But they solve completely different problems, have different lifecycles, and are registered in completely different ways.
Confusing one for the other leads to either using a network proxy for computation tasks, or trying to use a computation worker as a cache. Neither works well.
Web Workers: Offloading CPU Work
A Web Worker is a background thread for your application. It has no access to the DOM, no access to window, and limited access to browser APIs. What it does have is the ability to run JavaScript without blocking the main thread.
The main thread in the browser is responsible for parsing HTML, running JavaScript, handling user input, and painting frames. When any of these tasks take too long, the UI freezes. Web Workers move expensive computation off that thread:
// main.js — create the worker
const worker = new Worker('/workers/compute.js')
worker.postMessage({ data: largeArray })
worker.onmessage = (e) => {
console.log('Result:', e.data.result)
}
// compute.js — runs in worker thread
self.onmessage = (e) => {
const result = heavyComputation(e.data.data)
self.postMessage({ result })
}
The worker runs in a separate OS thread. The main thread sends data, the worker processes it, and the result is posted back — all without touching the event loop.
When to Use Web Workers
- Parsing large JSON or CSV files
- Running image processing or canvas operations
- Performing machine learning inference in the browser
- Cryptographic operations (hashing, encryption)
- Running a WASM module that needs CPU time
Service Workers: A Programmable Network Proxy
A Service Worker is a background script that sits between your web application and the network. It intercepts all fetch requests made by the page and can respond from cache, modify requests, or fall back to the network. It also persists across page loads — once registered, it stays active even when the tab is closed.
// Register in main thread
navigator.serviceWorker.register('/sw.js')
// sw.js — intercept fetch events
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request)
})
)
})
Service Workers are the foundation of Progressive Web Apps (PWAs). They enable offline functionality, background sync, and push notifications — capabilities that require the worker to outlive the page itself.
When to Use Service Workers
- Caching static assets for offline use
- Serving stale content while revalidating in the background
- Intercepting and modifying API requests
- Pushing notifications to users who aren't on the page
- Running background sync when connectivity is restored
The Key Differences
| Web Worker | Service Worker | |
|---|---|---|
| Purpose | Off-main-thread computation | Network proxy + offline |
| DOM access | No | No |
| Lifetime | Tied to the page | Persists across page loads |
| Communication | postMessage to/from page | postMessage, fetch events |
| HTTP interception | No | Yes |
| Registration | new Worker(url) | navigator.serviceWorker.register(url) |
| Multiple instances | Yes (one per Worker instance) | One per origin/scope |
Shared Workers
There is a third variant: SharedWorker. A Shared Worker is a Web Worker that can be connected to by multiple tabs or windows from the same origin simultaneously. Useful for coordinating state across tabs (e.g. a shared WebSocket connection) without each tab maintaining its own.
const shared = new SharedWorker('/workers/shared.js')
shared.port.postMessage({ type: 'PING' })
shared.port.onmessage = (e) => console.log(e.data)
Browser support for Shared Workers is good but not universal — Safari added support in 2021.
Communication Patterns
Both worker types communicate via postMessage. For large data (like typed arrays), you can transfer ownership using Transferable objects to avoid copying:
const buffer = new ArrayBuffer(1024 * 1024 * 50) // 50MB
worker.postMessage({ buffer }, [buffer]) // transfer, not copy
// buffer is now detached in main thread
For structured collaboration between multiple Workers and the main thread, the BroadcastChannel API provides a pub/sub interface that works across all contexts (pages, workers, service workers) on the same origin.
Conclusion
Web Workers and Service Workers solve different halves of frontend performance. Web Workers prevent the main thread from blocking by moving computation off it. Service Workers prevent network latency from degrading the user experience by acting as a local proxy. The choice between them is not about preference — it is determined entirely by what problem you are solving. Computation goes in a Web Worker. Network control goes in a Service Worker.
