Web Workers vs Service Workers: Different Tools for Different Problems

Web APIs & Async

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 WorkerService Worker
PurposeOff-main-thread computationNetwork proxy + offline
DOM accessNoNo
LifetimeTied to the pagePersists across page loads
CommunicationpostMessage to/from pagepostMessage, fetch events
HTTP interceptionNoYes
Registrationnew Worker(url)navigator.serviceWorker.register(url)
Multiple instancesYes (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.