Skip to main content
โšก Calmops

Service Workers: Lifecycle, Caching Strategies, and Offline Support

Introduction

app and the network โ€” enabling offline support, background sync, push notifications, and performance improvements through caching.

What Service Workers Can Do

  • Offline support โ€” serve cached content when the network is unavailable
  • Cache management โ€” control exactly what gets cached and for how long
  • Background sync โ€” defer actions until connectivity is restored
  • Push notifications โ€” receive messages even when the app isn’t open
  • Performance โ€” serve assets from cache instead of the network

Service Worker Lifecycle

Register โ†’ Download โ†’ Install โ†’ Activate โ†’ Idle โ†’ Fetch/Message/Push

er** โ€” your page registers the SW file 2. Download โ€” browser downloads the SW script 3. Install โ€” SW installs, install event fires (cache assets here) 4. Waiting โ€” new SW waits for old SW to release clients 5. Activate โ€” SW activates, activate event fires (clean old caches here) 6. Idle โ€” SW waits for events 7. Terminated โ€” browser may terminate idle SWs to save memory

Registering a Service Worker

// main.js โ€” register in your main page script
if ('serviceWorker' in navigator) {
    window.addEventListener('load', async () => {
        try {
            const registration = await navigator.serviceWorker.register('/sw.js', {
                scope: '/'  // controls which pages the SW manages
            });

            console.log('SW registered, scope:', registration.scope);

            // Listen for updates
            registration.addEventListener('updatefound', () => {
                const newSW = registration.installing;
e', () => {
                    console.log('SW state:', newSW.state);
                    // states: installing โ†’ installed โ†’ activating โ†’ activated
                });
            });

        } catch (error) {
            console.error('SW registration failed:', error);
        }
    });
}

Scope

The scope determines which pages the SW controls. /sw.js at the root controls all pages. /posts/sw.js only controls pages under /posts/:

// Only controls /posts/* pages
navigatviceWorker.register('/posts/sw.js', { scope: '/posts/' });

The Install Event: Pre-caching

Cache critical assets during installation:

// sw.js
const CACHE_NAME = 'myapp-v1';
const PRECACHE_ASSETS = [
    '/',
    '/index.html',
    '/styles/main.css',
    '/scripts/app.js',
    '/images/logo.png',
    '/offline.html'
];

self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then((cache) => {
                co assets');
                return cache.addAll(PRECACHE_ASSETS);
            })
            .then(() => self.skipWaiting())  // activate immediately
    );
});

event.waitUntil() keeps the SW alive until the promise resolves. self.skipWaiting() forces the new SW to activate without waiting for old clients to close.

The Activate Event: Cache Cleanup

Remove old caches when a new SW activates:

self.addEventListener('activate', (event) => {
    const CURRENT_CACHES = [CACHE_NAME];

 t.waitUntil(
        caches.keys()
            .then((cacheNames) =>
                Promise.all(
                    cacheNames
                        .filter((name) => !CURRENT_CACHES.includes(name))
                        .map((name) => {
                            console.log('Deleting old cache:', name);
                            return caches.delete(name);
                        })
                )
            )
            .then(() => self.clients.claim())  // take control of all clients
    );
});

self.clients.claim() makes the SW take control of all open pages immediately.

The Fetch Event: Caching Strategies

The fetch event intercepts all network requests. Choose a strategy based on the resource type:

Strategy 1: Cache First (Static Assets)

| fetch(event.request))event.s


## Debugging Service Workers

### Chrome DevTools

1. DevTools โ†’ Application โ†’ Service Workers
2. Check "Update on reload" during development
3. Click "Unregister" to remove the SW
4. Use "Bypass for network" to skip the SW temporarily

### Inspecting Caches

```javascript
// In DevTools console
const cacheNames = await caches.keys();
console.log(cacheNames);

const cache = await caches.open('myapp-v1');
const keys = await cache.keys();
keys.forEach(req => console.log(req.url));

Resource// Cache images for 30 days

registerRoute( ({ request }) => request.destination === ‘image’, new CacheFirst({ cacheName: ‘images’, plugins: [new ExpirationPlugin({ maxAgeSeconds: 30 * 24 * 60 * 60 })] }) );

// Network first for API registerRoute( ({ url }) => url.pathname.startsWith(’/api/’), new NetworkFirst({ cacheName: ‘api-cache’ }) );

// Stale while revalidate for pages registerRoute( ({ request }) => request.mode === ’navigate’, new StaleWhileRevalidate({ cacheName:ogle’s library that simplifies service worker development:

npm install workbox-webpack-plugin
# or for Vite:
npm install vite-plugin-pwa
// sw.js with Workbox
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';

// Pre-cache assets (injected by build tool)
precacheAndRoute(self.__WB_MANIFEST);

ATED' });
    });
});

// Service Worker โ†’ Specific Client (the sender)
self.addEventListener('message', (event) => {
    if (event.data.type === 'PING') {
        event.source.postMessage({ type: 'PONG' });
    }
});

// Page listening for SW messages
navigator.serviceWorker.addEventListener('message', (event) => {
    if (event.data.type === 'CACHE_UPDATED') {
        showUpdateBanner();
    }
});

Workbox is Go method: ‘POST’, body: JSON.stringify(item) }).then(() => removeFromIndexedDB(item.id)) )) ) ); } });


## Messaging Between Page and Service Worker

```javascript
// Page โ†’ Service Worker
navigator.serviceWorker.controller?.postMessage({
    type: 'SKIP_WAITING'
});

// Service Worker โ†’ All Clients
self.clients.matchAll().then((clients) => {
    clients.forEach((client) => {
        client.postMessage({ type: 'CACHE_UPDgify(data) });
    } catch {
        // Store for later and register sync
        await saveToIndexedDB(data);
        const registration = await navigator.serviceWorker.ready;
        await registration.sync.register('submit-form');
    }
}

// In sw.js
self.addEventListener('sync', (event) => {
    if (event.tag === 'submit-form') {
        event.waitUntil(
            getFromIndexedDB().then((items) =>
                Promise.all(items.map((item) =>
                    fetch('/api/submit', {
              e when the network is unavailable:

```javascript
self.addEventListener('fetch', (event) => {
    if (event.request.headers.get('accept')?.includes('text/html')) {
        event.respondWith(
            fetch(event.request)
                .catch(() => caches.match('/offline.html'))
        );
    }
});

Background Sync

Defer actions until connectivity is restored:

// In your page
async function submitForm(data) {
    try {
        await fetch('/api/submit', { method: 'POST', body: JSON.strinME);
        cache.put(request, response.clone());
        return response;
    } catch {
        return caches.match(request);
    }
}

async function staleWhileRevalidate(request) {
    const cache = await caches.open(CACHE_NAME);
    const cached = await cache.match(request);
    const networkPromise = fetch(request).then((response) => {
        cache.put(request, response.clone());
        return response;
    });
    return cached || networkPromise;
}

Offline Fallback Page

Show a custom offline pag if (event.request.headers.get(‘accept’)?.includes(’text/html’)) { event.respondWith(staleWhileRevalidate(event.request)); return; }

// Default: network first
event.respondWith(networkFirst(event.request));

});

async function cacheFirst(request) { const cached = await caches.match(request); return cached || fetch(request); }

async function networkFirst(request) { try { const response = await fetch(request); const cache = await caches.open(CACHE_NA); });


### Complete Strategy Router

```javascript
self.addEventListener('fetch', (event) => {
    const url = new URL(event.request.url);

    // Static assets: cache first
    if (url.pathname.match(/\.(css|js|png|jpg|svg|woff2)$/)) {
        event.respondWith(cacheFirst(event.request));
        return;
    }

    // API calls: network first
    if (url.pathname.startsWith('/api/')) {
        event.respondWith(networkFirst(event.request));
        return;
    }

    // HTML pages: stale while revalidate const networkFetch = fetch(event.request).then((response) => {
                    cache.put(event.request, response.clone());
                    return response;
                });
                return cached || networkFetch;  // return cache immediately if available
            })
        )
    );
});

Strategy 4: Cache Only (Offline-First)

Only serve from cache โ€” never hit the network:

self.addEventListener('fetch', (event) => {
    event.respondWith(caches.match(event.request)request, clone));
                    return response;
                })
                .catch(() => caches.match(event.request))  // fall back to cache
        );
    }
});

Strategy 3: Stale While Revalidate (Best for Most Content)

Serve from cache immediately, update cache in background:

self.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.open(CACHE_NAME).then((cache) =>
            cache.match(event.request).then((cached) => {
               
    );
});

Strategy 2: Network First (API Calls)

Try network, fall back to cache. Best for dynamic data:

self.addEventListener('fetch', (event) => {
    if (event.request.url.includes('/api/')) {
        event.respondWith(
            fetch(event.request)
                .then((response) => {
                    // Cache the fresh response
                    const clone = response.clone();
                    caches.open(CACHE_NAME).then((cache) => cache.put(
Serve from cache, fall back to network. Best for versioned assets (CSS, JS, images):

```javascript
self.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.match(event.request)
            .then((cached) => cached |

Comments