Frontend Engineering iconFrontend Engineering hover icon

Frontend Engineering

Service Workers: Network Interception vs. Application-Level Caching Trade-offs

JavaScriptJavaScript
TypeScriptTypeScript
WebpackWebpack

Key Takeaway

Service workers enable offline-first web applications through network-level request interception, but production success requires matching cache strategies to resource characteristics and implementing explicit update UX to navigate the complex activation lifecycle.

Request Interception at the Browser Level

Service workers intercept fetch requests before they reach the network layer, unlike "localStorage" or "IndexedDB" which only store data. Running in a separate worker thread, they persist across page reloads and tabs, enabling true offline functionality by returning cached responses, implementing retry logic, or constructing synthetic responses.

// Register service worker
navigator.serviceWorker.register('/sw.js').then(reg => {
  console.log('Service worker registered', reg.scope);
});

// Inside sw.js - intercept all fetch requests
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      return cached || fetch(event.request);
    })
  );
});

Cache Strategy Selection Matrix

Different resource types need different caching approaches. "Cache First" serves cached content instantly, ideal for static assets but risks stale data. "Network First" prioritizes freshness for API calls but adds latency. "Stale While Revalidate" serves cache immediately while fetching updates in background, offering best perceived performance but requiring careful state management.

// Cache First - static assets
if (event.request.url.includes('/static/')) {
  event.respondWith(
    caches.match(event.request)
      .then(cached => cached || fetch(event.request))
  );
}

// Network First - API calls
if (event.request.url.includes('/api/')) {
  event.respondWith(
    fetch(event.request).catch(() => caches.match(event.request))
  );
}

The Service Worker Lifecycle State Machine

New service workers enter "installed" state but wait until all tabs using the old version close before activating. This waiting period can last days if users keep tabs open. Calling "skipWaiting()" forces immediate activation but creates race conditions where in-flight requests might be handled by the old worker while new page loads use the updated version, causing data schema mismatches.

Applied Insight: Update UX and Activation Patterns

Production PWAs require explicit update prompts. Detect waiting workers and communicate via "postMessage()" to trigger "skipWaiting()" followed by "clients.claim()" for immediate control.

// Detect updates
navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.addEventListener('updatefound', () => {
    const newWorker = reg.installing;
    newWorker.addEventListener('statechange', () => {
      if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
        showUpdatePrompt(); // Show banner
      }
    });
  });
});

// Inside sw.js
self.addEventListener('message', event => {
  if (event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

© 2025 BeautifulCode. All rights reserved.