Frontend Engineering/
Lesson

Think of a Service WorkerWhat is service worker?A background script the browser runs separately from your web page that intercepts network requests, enabling offline support and caching. as a tiny server living inside the user's browser. Every network request your page makes passes through it first. It can serve a cached response, modify the request, or let it through to the real network. That single capability is what enables offline support, blazing-fast repeat loads, and background syncWhat is background sync?A Service Worker API that queues network requests made while offline and replays them automatically when connectivity is restored..

How a Service WorkerWhat is service worker?A background script the browser runs separately from your web page that intercepts network requests, enabling offline support and caching. differs from regular JavaScript

Regular JavaScript runs in the main thread alongside your UI. If it blocks, the page freezes. A Service Worker runs in a completely separate worker thread, it has no DOMWhat is dom?The Document Object Model - the browser's live representation of your HTML page as a tree of objects that JavaScript can read and modify., no window, and no localStorage. It can be terminated by the browser at any time and restarted on demand, so you can't rely on in-memory state between events.

What it can do:

  • Intercept and respond to any network request made by pages in its scopeWhat is scope?The area of your code where a variable is accessible; variables declared inside a function or block are invisible outside it.
  • Cache responses using the Cache APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses.
  • Receive push messages from a server even when the page is closed
  • Run background syncWhat is background sync?A Service Worker API that queues network requests made while offline and replays them automatically when connectivity is restored. tasks when connectivity is restored
Service Workers only work over HTTPS. The exception is localhost, which the browser treats as secure for development purposes. If you try to register a SW on a plain HTTP production site, the browser silently refuses.
02

The Service WorkerWhat is service worker?A background script the browser runs separately from your web page that intercepts network requests, enabling offline support and caching. lifecycle

Understanding the lifecycle is the key to avoiding the most common bugs. The states go in this order:

navigator.serviceWorker.register('/sw.js')
        |
        v
   [Parsed & loaded]
        |
        v
   [install event]  ← cache static assets here
        |
        v
   [activate event] ← clean up old caches here
        |
        v
   [idle] ← browser can terminate here
        |
        v
[fetch / push / sync events] ← handle requests here

Registration

You register the Service Worker once from your main JavaScript. The browser downloads, parses, and installs it. On subsequent page loads it's already registered, so this line is fast.

// In your main app JS or inline script
if ('serviceWorker' in navigator) {
  navigator.serviceWorker
    .register('/sw.js') // path must be absolute
    .then(registration => {
      console.log('SW registered! Scope:', registration.scope);
    })
    .catch(error => {
      console.error('SW registration failed:', error);
    });
}

The install event

install fires once when the Service Worker is first registered (or when the file changes). This is your chance to pre-cache the shell of your app, the files you want available offline from the very first visit.

// sw.js
const CACHE_NAME = 'my-app-v1';
const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js',
  '/offline.html',
  '/icon-192.png'
];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => {
      console.log('[SW] Caching static assets');
      return cache.addAll(STATIC_ASSETS);
    })
  );

  // Skip the waiting phase - become active immediately
  self.skipWaiting();
});

event.waitUntil() tells the browser not to move to activate until the promiseWhat is promise?An object that represents a value you don't have yet but will get in the future, letting your code keep running while it waits. resolves. If any file in addAll() fails to fetch, the whole install fails and the SW won't activate, useful, because you won't serve a broken cached shell.

The activate event

activate fires after installation (and after any previously active SW has finished). Clean up stale caches here so you don't waste the user's storage.

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames
          .filter(name => name !== CACHE_NAME) // keep only current version
          .map(name => {
            console.log('[SW] Deleting old cache:', name);
            return caches.delete(name);
          })
      );
    })
  );

  // Take control of all open pages immediately
  self.clients.claim();
});

The fetch event

This is where the real work happens. Every request the browser makes, HTML, CSS, images, APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. calls, triggers this event.

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(cachedResponse => {
      // Found in cache - return it
      if (cachedResponse) {
        return cachedResponse;
      }

      // Not in cache - fetch from network, then cache the response
      return fetch(event.request).then(networkResponse => {
        // Only cache valid responses
        if (!networkResponse || networkResponse.status !== 200) {
          return networkResponse;
        }

        const responseToCache = networkResponse.clone(); // clone before consuming
        caches.open(CACHE_NAME).then(cache => {
          cache.put(event.request, responseToCache);
        });

        return networkResponse;
      }).catch(() => {
        // Network failed and nothing in cache - show offline page
        if (event.request.mode === 'navigate') {
          return caches.match('/offline.html');
        }
      });
    })
  );
});
You must clone a response before reading it. Response objects are streams, once consumed they're gone. Call .clone() before passing to the cache and returning to the page.
03

Debugging in DevTools

Chrome DevTools has a dedicated panel for Service Workers:

PanelWhat you can do
Application → Service WorkersSee registered SWs, force update, unregister
Application → Cache StorageBrowse cached entries, delete individual items
Network tab → "Offline" checkboxSimulate no connectivity
Application → "Update on reload"Always install the latest SW version during dev
04

Common mistakes

Use an absolute path when registering, a relative path can put the SW in the wrong scopeWhat is scope?The area of your code where a variable is accessible; variables declared inside a function or block are invisible outside it.:

// Wrong - scope is /js/, not /
navigator.serviceWorker.register('js/sw.js');

// Correct - scope is /
navigator.serviceWorker.register('/sw.js');

Always call event.waitUntil() in install and activate, and always call event.respondWith() in fetch. Without them, the browser won't wait for your async work to finish.

05

Quick reference

EventWhen it firesWhat to do
installFirst registration or SW file changedPre-cache static assets
activateAfter old SW is doneDelete stale caches
fetchEvery network request in scopeImplement your caching strategy
pushPush message receivedShow notification
syncBackground sync triggeredSend queued offline data
javascript
// Complete Service Worker, sw.js

const CACHE_NAME = 'my-pwa-v1';
const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js',
  '/offline.html',
  '/icon-192.png'
];

// Installation: cache static resources
self.addEventListener('install', event => {
  console.log('[SW] Installing...');

  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('[SW] Caching resources');
        return cache.addAll(STATIC_ASSETS);
      })
      .catch(err => console.error('[SW] Cache error:', err))
  );

  // Force immediate activation
  self.skipWaiting();
});

// Activation: clean old caches
self.addEventListener('activate', event => {
  console.log('[SW] Activating...');

  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames
          .filter(name => name !== CACHE_NAME)
          .map(name => {
            console.log('[SW] Deleting cache:', name);
            return caches.delete(name);
          })
      );
    })
  );

  // Take control immediately
  self.clients.claim();
});

// Fetch: intercept requests
self.addEventListener('fetch', event => {
  console.log('[SW] Fetch:', event.request.url);

  event.respondWith(
    caches.match(event.request)
      .then(cachedResponse => {
        // If in cache, return
        if (cachedResponse) {
          return cachedResponse;
        }

        // Otherwise fetch from network
        return fetch(event.request)
          .then(networkResponse => {
            // Don't cache if not valid
            if (!networkResponse || networkResponse.status !== 200) {
              return networkResponse;
            }

            // Clone and cache
            const responseToCache = networkResponse.clone();
            caches.open(CACHE_NAME).then(cache => {
              cache.put(event.request, responseToCache);
            });

            return networkResponse;
          })
          .catch(() => {
            // Offline fallback
            if (event.request.mode === 'navigate') {
              return caches.match('/offline.html');
            }
          });
      })
  );
});

// Background sync
self.addEventListener('sync', event => {
  if (event.tag === 'sync-data') {
    event.waitUntil(syncData());
  }
});

// Push notifications
self.addEventListener('push', event => {
  const options = {
    body: event.data.text(),
    icon: '/icon-192.png',
    badge: '/badge-72.png'
  };

  event.waitUntil(
    self.registration.showNotification('My PWA', options)
  );
});