DEV Community

ZeeshanAli-0704
ZeeshanAli-0704

Posted on

Comparison of Web Worker, SharedWorker, and Service Worke

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 });
  }
};
Enter fullscreen mode Exit fullscreen mode
  • 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();
Enter fullscreen mode Exit fullscreen mode

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;
    }
  };
};
Enter fullscreen mode Exit fullscreen mode
  • 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 });
}
Enter fullscreen mode Exit fullscreen mode

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 });
  }
});
Enter fullscreen mode Exit fullscreen mode
  • 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' });
  }
}
Enter fullscreen mode Exit fullscreen mode

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)