DNA🌐 Web BrowserService Workers & Offline Strategies
👑ApexBrowserService WorkersPWACaching

Service Workers & Offline Strategies

Service workers are programmable network proxies that run in a separate thread. They enable offline experiences, background sync, push notifications, and intelligent caching strategies.

Service Workers & Offline Strategies

Service workers sit between your application and the network. They intercept every fetch request, enabling cache-first strategies, background sync, and genuine offline support. They're the foundation of Progressive Web Apps.

Lifecycle

Service workers have a strict lifecycle that prevents broken deployments:

Registration → Installation → Waiting → Activation → Controlling
if ('serviceWorker' in navigator) {
  const registration = await navigator.serviceWorker.register('/sw.js', {
    scope: '/',
  });
}

Installation Phase

The install event fires once when the SW is first registered (or when the file changes). Use it to pre-cache critical resources:

const CACHE_NAME = 'app-v1';
const PRECACHE_URLS = [
  '/',
  '/styles/main.css',
  '/scripts/app.js',
  '/offline.html',
];
 
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
  );
});

Activation Phase

The activate event fires after installation when no other SW version is controlling pages. Clean up old caches here:

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(
        keys
          .filter((key) => key !== CACHE_NAME)
          .map((key) => caches.delete(key))
      )
    )
  );
});

Skipping the Waiting Phase

By default, a new SW waits until all tabs using the old SW are closed. To activate immediately:

self.addEventListener('install', (event) => {
  self.skipWaiting();
  event.waitUntil(/* precaching */);
});
 
self.addEventListener('activate', (event) => {
  event.waitUntil(self.clients.claim());
});

Warning: This can cause inconsistencies if old-cached pages use new-version assets.

Caching Strategies

Cache First (Cache Falling Back to Network)

Best for: static assets (CSS, JS, images, fonts)

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return cached || fetch(event.request).then((response) => {
        const clone = response.clone();
        caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
        return response;
      });
    })
  );
});

Network First (Network Falling Back to Cache)

Best for: API responses, frequently changing content

self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request)
      .then((response) => {
        const clone = response.clone();
        caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
        return response;
      })
      .catch(() => caches.match(event.request))
  );
});

Stale-While-Revalidate

Best for: content that should be fresh but doesn't need to block rendering

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.open(CACHE_NAME).then((cache) =>
      cache.match(event.request).then((cached) => {
        const fetchPromise = fetch(event.request).then((response) => {
          cache.put(event.request, response.clone());
          return response;
        });
        return cached || fetchPromise;
      })
    )
  );
});

Strategy Router

self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);
 
  if (request.destination === 'image') {
    event.respondWith(cacheFirst(request));
  } else if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(request));
  } else if (request.destination === 'document') {
    event.respondWith(networkFirst(request));
  } else {
    event.respondWith(staleWhileRevalidate(request));
  }
});

Background Sync

Execute failed operations when connectivity returns:

// App code
async function sendMessage(data) {
  try {
    await fetch('/api/messages', { method: 'POST', body: JSON.stringify(data) });
  } catch {
    const registration = await navigator.serviceWorker.ready;
    await registration.sync.register('send-messages');
    await saveToIndexedDB('pending-messages', data);
  }
}
 
// Service worker
self.addEventListener('sync', (event) => {
  if (event.tag === 'send-messages') {
    event.waitUntil(
      getFromIndexedDB('pending-messages').then((messages) =>
        Promise.all(messages.map((msg) =>
          fetch('/api/messages', { method: 'POST', body: JSON.stringify(msg) })
        ))
      ).then(() => clearIndexedDB('pending-messages'))
    );
  }
});

Push Notifications

// App code — subscribe
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
  userVisibleNotification: true,
  applicationServerKey: vapidPublicKey,
});
 
// Service worker — receive
self.addEventListener('push', (event) => {
  const data = event.data?.json() ?? {};
  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icon-192.png',
      badge: '/badge-72.png',
      data: { url: data.url },
    })
  );
});
 
self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  event.waitUntil(
    clients.openWindow(event.notification.data.url)
  );
});

Update Strategy

// Check for updates periodically
navigator.serviceWorker.ready.then((registration) => {
  setInterval(() => registration.update(), 60 * 60 * 1000);
});
 
// Notify users of updates
navigator.serviceWorker.addEventListener('controllerchange', () => {
  if (confirm('New version available. Reload?')) {
    window.location.reload();
  }
});

Limitations

  • HTTPS only — Service workers only work on localhost and HTTPS origins
  • No DOM access — They run in a separate thread, communicate via postMessage
  • No synchronous APIs — Everything is promise-based
  • Scope restriction — A SW at /app/sw.js can only control /app/* URLs
  • Storage limits — Cache Storage is subject to browser storage quotas

Interview Signal

Senior candidates demonstrate:

  1. Lifecycle mastery — install → waiting → activate, skipWaiting/claim trade-offs
  2. Strategy selection — Which caching strategy for which content type, and why
  3. Offline-first thinking — Background sync, IndexedDB fallback, graceful degradation
  4. Update management — Cache versioning, old cache cleanup, user notification
  5. Security model — HTTPS requirement, scope restrictions, same-origin fetch interception only