How to Add Offline Support to a Progressive Web App

A person holding a smartphone close up showing a mobile app interface

Most web apps fail visibly when the network disappears. The screen goes blank, the browser throws a "no connection" page, and the user closes the tab. It is one of the most avoidable failure modes in modern web development, and the tooling to prevent it has been stable and well-supported for years.

Progressive Web Apps (PWAs) can work without a connection because they use service workers -- background scripts that intercept network requests, serve assets from cache, and handle fallback logic when the server is unreachable. This guide walks through what you need to implement offline support: service worker registration, cache strategies, offline fallback pages, background sync, and the development workflow for testing and debugging the whole thing.

The MDN Web Docs are the authoritative reference for the Service Worker API and Cache Storage API. The web.dev site from Google covers progressive enhancement patterns and offline design in considerable depth, including decision guides for which caching strategy fits which type of content.

App store interface on a tablet device
Photo by El Jundi on Pexels

What a Service Worker Actually Is

A service worker is a JavaScript file that runs in a separate thread from your web page, with no access to the DOM. It acts as a programmable proxy between your application and the network. When your page requests a resource -- a script, an image, a JSON API response -- the service worker intercepts the request and decides whether to fetch from the network, serve from cache, or do both.

This interception happens at the browser level, not the application level. The service worker persists even after the tab is closed, which is why it can handle push notifications and background sync in addition to caching. It has its own lifecycle -- install, activate, and fetch -- that is separate from the page lifecycle, which gives it a level of control and persistence that in-page JavaScript does not have.

Service workers require HTTPS (or localhost for development). They are scoped to the directory they live in -- a service worker at /sw.js controls all pages on the origin, while one at /app/sw.js controls only pages under /app/.

Step 1: Register the Service Worker

Registration happens in your main JavaScript file. The browser will download and install the service worker the first time a user visits your app.

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(registration => {
        console.log('SW registered:', registration.scope);
      })
      .catch(error => {
        console.error('SW registration failed:', error);
      });
  });
}

The feature detection ensures older browsers are not affected. Wrapping registration in the load event prevents the service worker from competing with initial page resources during first paint.

Step 2: Choose a Cache Strategy

Caching strategy is the most important architectural decision in offline support. The appropriate strategy depends on how often content changes and how much the app degrades when content is stale.

Cache First (Offline First): Serve from cache, fall back to network if the cache misses. Use for assets that rarely change: fonts, icons, CSS, JavaScript bundles. This strategy gives the fastest response times but can serve stale content if cache invalidation is not managed carefully.

Network First (Fresh First): Try the network, fall back to cache if the network fails. Use for content that should be current: API responses, news feeds, dynamic data. This incurs a network round-trip on every request when online, so combine it with a timeout for slow connections.

Stale While Revalidate: Serve from cache immediately, update the cache in the background. Use for content where speed matters more than freshness: profile data, settings, non-critical API endpoints. The user always gets a fast response; the background update ensures the next visit gets fresh content.

Choosing the wrong strategy is the most common source of subtle offline bugs. A news app using Cache First will serve week-old headlines to users who are online, which looks like a bug rather than a feature. A document editor using Network First will fail entirely when the user loses connectivity mid-session. Think through the user experience for each content type independently.

Terminal screen showing monospace code output
Photo by Mathias Reding on Pexels

Step 3: Precache Critical Assets on Install

The install event fires when the browser first installs the service worker. This is where you precache the static assets your app needs to function offline.

const CACHE_NAME = 'app-shell-v1';
const ASSETS_TO_CACHE = ['/', '/index.html', '/styles/main.css', '/scripts/app.js', '/offline.html'];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(ASSETS_TO_CACHE))
      .then(() => self.skipWaiting())
  );
});

The self.skipWaiting() call makes the new service worker take over immediately. Pair it with clients.claim() in the activate handler to ensure all open tabs are controlled immediately.

Step 4: Build an Offline Fallback Page

An offline fallback page is served when a navigation request fails and no cached version exists. It needs to work entirely from cached assets.

self.addEventListener('fetch', event => {
  if (event.request.mode === 'navigate') {
    event.respondWith(
      fetch(event.request).catch(() => caches.match('/offline.html'))
    );
  }
});

The fallback page should confirm the user is offline, explain what will work in offline mode, and give clear instructions for when connection returns. An offline page that explains the situation clearly is significantly better than a browser-default error screen and keeps users from assuming the app is broken permanently.

"The offline experience is a feature, not a fallback. Apps that handle disconnection gracefully keep users; apps that crash on it lose them permanently." - Dennis Traina, founder of 137Foundry

Step 5: Background Sync for Offline Actions

Service workers enable background sync, which lets your app queue user actions when offline and execute them when the connection restores. This is valuable for form submissions, message sends, and any write operation the user performs while disconnected.

The Background Sync API requires registration through the service worker. The pattern: the app registers a sync tag when it performs an action offline; the service worker's sync event fires when connectivity is restored and replays the queued request. For simpler use cases without full Background Sync API support, IndexedDB is a reliable fallback -- store the action locally, detect connectivity via the online event, and replay the queue on reconnect.

Browser compatibility for Background Sync and related APIs is tracked at caniuse.com. The W3C specification covers the full technical detail for Background Sync and the Push API. Both APIs have strong support in Chromium-based browsers; Firefox and Safari have varying levels of support for the less-common APIs.

Step 6: Handle Cache Versioning and Cleanup

When you update your app, old caches accumulate. The activate event is where you clean them up. Update CACHE_NAME whenever your assets change, and tie cache version strings to your build process rather than updating them manually -- a build hash in the cache name eliminates the risk of forgetting to increment it when shipping updates.

The activate handler pattern uses caches.keys() to find all caches, filters out the current ones, and deletes the rest. This ensures users do not accumulate gigabytes of stale cache over time, which is a real concern for apps with large asset bundles.

Using Workbox to Manage Complexity

Writing service worker logic manually is manageable for simple cases. For applications with complex routing, multiple cache strategies, and background sync, Workbox (a library from Google) provides a higher-level API that handles the boilerplate.

The Google Developers documentation for Workbox covers route matching, strategy configuration, and the workbox-build tooling that generates service worker files from a manifest. Workbox makes cache versioning and precaching significantly less error-prone and is worth the added dependency for production PWAs that need to handle dozens of route patterns across multiple cache strategies.

Testing Offline Behavior

Chrome DevTools has specific tooling for service worker testing. In the Application panel, you can inspect registered service workers, force updates, unregister, trigger lifecycle events, and check what is in each named cache. The Offline checkbox in the Network panel simulates disconnection without actually dropping your network.

Test the complete set of offline scenarios: first visit (nothing cached), returning visit with cached assets, navigation to an uncached page, and a form submission while offline. Each scenario surfaces different failure modes that are easy to miss in development where the network is always available.

Pitfalls to Avoid

Cache versioning gaps. If you update your CSS but not your CACHE_NAME, users get new HTML with old styles. Automate cache versioning with your build tool.

Caching POST requests. The Cache Storage API only supports GET requests. Handle write operations at the application level with IndexedDB if you need offline persistence.

Infinite redirect loops. A service worker that intercepts redirected requests and re-fetches them can create loops. Test navigation carefully with redirect scenarios before shipping.

Missing CORS headers. Cross-origin responses without proper CORS headers cannot be read by the service worker. For opaque responses, the cache stores them but you cannot inspect the response body -- which makes debugging difficult.

What This Enables Beyond Offline

Offline support via service workers is foundational to other PWA capabilities. Background sync, push notifications, and install-to-home-screen behavior all depend on having a service worker registered and the caching infrastructure in place.

137Foundry's web development services incorporate PWA patterns into client projects regularly. The offline-first approach is particularly valuable for apps targeting users with variable connectivity -- field service tools, delivery tracking, content platforms, and any application where connection reliability is not guaranteed.

The 137Foundry services hub covers web development, AI integration, and technical SEO as adjacent capabilities when you are building production-grade applications. Explore 137Foundry for engineering consultation on PWA architecture and service worker implementation for your specific requirements.

Phone displaying home screen icons in a grid layout
Photo by kaboompics on Pixabay

Need help with Web Development?

137Foundry builds custom software, AI integrations, and automation systems for businesses that need real solutions.

Book a Free Consultation View Services