The web works when you are online. That is the baseline. The web that works when you are offline is the web that wins. Service Workers are the browser API that makes this possible. They are not a framework. They are not a library. They are a programmable network proxy that sits between your application and the internet.
A Service Worker is a programmable network proxy. Use it like one.
The lifecycle is simple but unforgiving. Install, activation, fetch. During install, you cache static assets. During activation, you clean up old caches. During fetch, you intercept network requests and decide whether to serve from cache, network, or a combination. Get the lifecycle wrong and your users see stale content indefinitely.
// service-worker.js
const CACHE_NAME = 'v2';
const STATIC_ASSETS = [
    '/',
    '/styles.css',
    '/app.js',
    '/offline.html'
];

// Install: cache static assets
self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => cache.addAll(STATIC_ASSETS))
            .then(() => self.skipWaiting())
    );
});

// Activate: clean old caches
self.addEventListener('activate', (event) => {
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames
                    .filter(name => name !== CACHE_NAME)
                    .map(name => caches.delete(name))
            );
        }).then(() => self.clients.claim())
    );
});
The fetch strategy depends on the resource type. For navigation requests — the HTML pages — use a network-first strategy with a cache fallback. Users should always see the freshest content if they are online. For static assets like CSS and JS, use cache-first. These files are versioned by filename, so the cache is never stale.
// Fetch: different strategies for different requests
self.addEventListener('fetch', (event) => {
    const { request } = event;
    const url = new URL(request.url);

    // Navigation requests: network first, cache fallback
    if (request.mode === 'navigate') {
        event.respondWith(
            fetch(request)
                .then(response => {
                    const clone = response.clone();
                    caches.open(CACHE_NAME).then(cache =>
                        cache.put(request, clone)
                    );
                    return response;
                })
                .catch(() => caches.match(request)
                    .then(response => response || caches.match('/offline.html'))
                )
        );
        return;
    }

    // Static assets: cache first
    if (request.destination === 'style' || request.destination === 'script') {
        event.respondWith(
            caches.match(request).then(cached =>
                cached || fetch(request).then(response => {
                    const clone = response.clone();
                    caches.open(CACHE_NAME).then(cache =>
                        cache.put(request, clone)
                    );
                    return response;
                })
            )
        );
    }
});
Background Sync is the hidden gem. When a user submits a form while offline, the Service Worker queues the request. When connectivity returns, it retries automatically. The user does not need to resubmit. The application does not lose data. This is how native apps work, and now the web can do it too.
// Queue failed requests for background sync
self.addEventListener('sync', (event) => {
    if (event.tag === 'sync-comments') {
        event.waitUntil(syncPendingComments());
    }
});

async function syncPendingComments() {
    const db = await openDB('pending-comments', 1);
    const comments = await db.getAll('comments');

    for (const comment of comments) {
        try {
            await fetch('/api/comments', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(comment)
            });
            await db.delete('comments', comment.id);
        } catch (err) {
            console.error('Sync failed for comment:', comment.id);
        }
    }
}
Service Workers are not just for offline support. They enable push notifications, background sync, and fine-grained caching strategies. But their real power is reliability. A web app with a Service Worker does not break when the network does. It degrades gracefully. It keeps working. That is the standard users expect. That is the standard we should ship.