Service workers don’t actually make your app "offline" in the sense of magically working without a network; they intercept network requests and serve cached assets.

Imagine you’re building a web app and want it to feel more like a native app – fast, reliable, and even usable without a perfect internet connection. That’s where Progressive Web Apps (PWAs) and service workers come in.

Let’s see a service worker in action. Here’s a basic service-worker.js file:

const CACHE_NAME = 'my-app-cache-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js',
  '/images/logo.png'
];

// Install event: Cache essential assets
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});

// Fetch event: Intercept network requests
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // Cache hit - return response
        if (response) {
          return response;
        }

        // Not in cache - fetch from network and cache it
        return fetch(event.request).then(
          response => {
            // Check if we received a valid response
            if (!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }

            const responseToCache = response.clone();

            caches.open(CACHE_NAME)
              .then(cache => {
                cache.put(event.request, responseToCache);
              });

            return response;
          }
        );
      })
  );
});

// Activate event: Clean up old caches
self.addEventListener('activate', event => {
  const cacheWhitelist = [CACHE_NAME]; // Keep only the current cache
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            // If not in the whitelist, delete the cache
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

And here’s how you’d register it in your app.js:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js')
      .then(registration => {
        console.log('ServiceWorker registration successful with scope: ', registration.scope);
      })
      .catch(err => {
        console.log('ServiceWorker registration failed: ', err);
      });
  });
}

This setup solves the problem of unreliable network connections for your web application. When a user visits your site, the install event fires, and the service worker caches the urlsToCache (your app’s essential assets like HTML, CSS, JS, and images). Later, when the app needs to fetch any of these resources, the fetch event intercepts the request. It first checks if the resource is already in the cache (caches.match). If it is (a cache hit), it serves the cached version directly, bypassing the network entirely. If it’s not in the cache (a cache miss), it fetches the resource from the network, caches it for future use, and then serves it to the user. The activate event ensures that as you update your service worker and cache names, old, unused caches are automatically cleaned up, preventing disk space bloat.

The core problem this addresses is the inherent latency and unreliability of the network. By caching critical assets, you can provide an experience that feels instantaneous on repeat visits and remains functional even when the user is offline. This dramatically improves perceived performance and user engagement, especially on mobile devices where network conditions can be highly variable.

Internally, service workers are JavaScript files that run in the background, separate from your web page. They act as a programmable proxy between the browser and the network. You control what gets cached, when it gets cached, and how it’s served during fetch events. This programmability is what makes them so powerful, allowing for strategies like cache-first (try cache, then network), network-first (try network, then cache), or stale-while-revalidate. The event.waitUntil() method is crucial; it ensures that long-running operations like cache opening or adding files aren’t terminated prematurely by the browser.

The most surprising thing about service workers is how much control they give you over the caching and network lifecycle, to the point where you can completely simulate offline behavior or implement sophisticated pre-caching strategies that load your app’s shell instantly. You can even use them for push notifications, background sync, and more, all managed by this single background script.

The next logical step after mastering caching is to implement background synchronization.

Want structured learning?

Take the full Webpack course →