React PWA: Offline-First with Workbox and next-pwa
AI generated
</>
{ }
React · PWA · Workbox · Offline-First
React PWA: Offline-First
with Workbox and next-pwa

A React app that shows a blank screen the moment the network drops is not a Progressive Web App, it is a website with an icon. Genuine offline-first operation requires deliberate caching strategies, background sync and a service worker that knows exactly what it may cache and what it must not.

18 min read Workbox · next-pwa · Service Worker · Background Sync · Cache API React 18+ · Next.js 14+ · Workbox 7

1. What sets a genuine PWA apart from a normal web app

The term Progressive Web App is often equated with "installable". That falls short. An installable app that shows nothing without a network connection fulfills none of the promises a PWA is actually supposed to make: reliable loading, regardless of the user's network situation. The technical foundation for that is not a web app manifest, it is a service worker with a well thought out caching strategy.

Offline-first does not mean "show an offline page when there is no network". It means the app is served from the cache by default and the network is only used for updates. This approach not only improves resilience against network outages, it also significantly improves perceived load speed, since assets come from the local cache instead of the network. For React apps this pattern can be implemented systematically with Workbox, and with next-pwa for Next.js projects.

2. Service worker: lifecycle and registration

A service worker is a JavaScript script the browser runs in the background, separate from the main thread, without access to the DOM. It acts as a network proxy between the app and the server and can intercept requests, answer them from the cache, or modify them. Its lifecycle consists of the phases install, activate and fetch. During install, assets are precached; during activate, old caches are removed; the fetch event handles every outgoing request.

The service worker is registered when the app starts. Important: the service worker must only be registered after the page has loaded, otherwise it competes with the initial page load for bandwidth. The standard pattern is therefore a registration in the load event. For React apps built with Create React App, the serviceWorkerRegistration.ts package exists; Next.js projects delegate registration to next-pwa, which integrates it into the build automatically. The service worker must be served from the root scope of the domain, since its scope is defined by its path.


// service-worker-registration.ts - Register SW only after page load
// to avoid competing with initial page resources

export function register(): void {
  if (
    typeof window !== 'undefined' &&
    'serviceWorker' in navigator &&
    process.env.NODE_ENV === 'production'
  ) {
    window.addEventListener('load', () => {
      navigator.serviceWorker
        .register('/sw.js')
        .then((registration) => {
          console.log('[SW] Registered, scope:', registration.scope);

          // Detect available update
          registration.addEventListener('updatefound', () => {
            const worker = registration.installing;
            if (!worker) return;
            worker.addEventListener('statechange', () => {
              if (
                worker.state === 'installed' &&
                navigator.serviceWorker.controller
              ) {
                // New version available - notify user
                dispatchEvent(new CustomEvent('sw-update-available'));
              }
            });
          });
        })
        .catch((err) => console.error('[SW] Registration failed:', err));
    });
  }
}

3. Workbox: caching strategies in detail

Workbox is Google's library for service worker logic and abstracts the raw Cache API into reusable strategies. The most important decision when building a PWA is which strategy fits which asset type. There are five basic strategies: Cache First, Network First, Stale While Revalidate, Network Only and Cache Only. Each has its place, and the usual mistake is using the wrong one, for instance treating API calls with Cache First and serving stale data as a result.

Cache First suits fonts, icons and static images that never change. Stale While Revalidate is ideal for JavaScript and CSS bundles: the user immediately gets the cached asset while a new version loads in the background. Network First is suited to API responses that must be current but should fall back to the cache when offline. Configuration with Workbox happens directly in the service worker file and is integrated into the build process via workbox-webpack-plugin or the Workbox CLI.

4. next-pwa: integration into Next.js projects

next-pwa is a Next.js plugin that automatically integrates Workbox into the build process. At build time it generates an optimized service worker from the Workbox build and inserts it into the public folder. Configuration happens in next.config.js via a handful of parameters. It is essential to disable PWA support in development mode, since service workers interfere with hot module replacement and make debugging considerably harder.

next-pwa handles precaching of all Next.js chunks automatically. For custom routes and API endpoints, runtime caching rules must be defined explicitly. The library also supports the Workbox InjectManifest mode, where you write the service worker file yourself and only the precaching manifest injection is handled for you. That gives full control over every strategy while retaining the build integration.


// next.config.js - next-pwa configuration with Workbox runtime caching

const withPWA = require('next-pwa')({
  dest: 'public',
  disable: process.env.NODE_ENV === 'development', // Disable in dev
  register: true,
  skipWaiting: true,
  // Custom runtime caching rules
  runtimeCaching: [
    {
      // Cache API responses with Network First (fallback to cache)
      urlPattern: /^https:\/\/api\.mironsoft\.de\/.*/i,
      handler: 'NetworkFirst',
      options: {
        cacheName: 'api-cache',
        expiration: { maxEntries: 50, maxAgeSeconds: 300 }, // 5 min TTL
        networkTimeoutSeconds: 10,
        cacheableResponse: { statuses: [0, 200] },
      },
    },
    {
      // Stale While Revalidate for JS/CSS bundles
      urlPattern: /\/_next\/static\/.*/i,
      handler: 'StaleWhileRevalidate',
      options: { cacheName: 'static-resources' },
    },
    {
      // Cache First for images (long TTL)
      urlPattern: /\.(?:png|jpg|jpeg|webp|avif|svg|ico)$/i,
      handler: 'CacheFirst',
      options: {
        cacheName: 'image-cache',
        expiration: { maxEntries: 200, maxAgeSeconds: 2592000 }, // 30 days
      },
    },
  ],
});

module.exports = withPWA({ reactStrictMode: true });

5. Precaching: securing the app shell and critical assets

Precaching is the centerpiece of the offline-first approach. During service worker install, all critical assets are loaded into the cache before the user ever needs them. The result: on the next visit, whether online or offline, every app shell asset is available immediately. Workbox manages the precaching manifest automatically and invalidates entries when a file's content hash changes. That prevents both stale caches and unnecessary re-downloads of unchanged files.

For Next.js apps, the precaching manifest contains every generated chunk, CSS file and static asset. Dynamic routes need special consideration: for a route's shell to be precached, the route must be known at build time. Unknown routes can be handled via runtime caching with Network First, so they get cached on the first visit and are available offline afterward. The combination of app-shell precaching and route-level runtime caching covers most scenarios.

6. Background sync: replaying offline actions

Background Sync solves the problem that users cannot send data while offline, so form submissions, likes or orders fail. With the Background Sync API, the service worker registers a sync tag when a network request fails offline. As soon as the connection is restored, the browser fires the sync event and the service worker replays the action. The user notices nothing: their action was merely delayed, not discarded.

The implementation consists of two parts: in the app code, the failed request is stored in IndexedDB and a sync tag is registered. In the service worker, the sync event reads IndexedDB and retries every pending request. Workbox provides a ready-made queue implementation via workbox-background-sync, sparing you the manual management of IndexedDB. For critical data such as orders, server-side idempotency is also recommended, since the sync event can fire more than once.


// custom-sw.ts - Background Sync with Workbox Queue
import { BackgroundSyncPlugin } from 'workbox-background-sync';
import { registerRoute } from 'workbox-routing';
import { NetworkOnly } from 'workbox-strategies';
import { precacheAndRoute } from 'workbox-precaching';

// Precache all assets injected by Workbox build
declare const self: ServiceWorkerGlobalScope & { __WB_MANIFEST: any[] };
precacheAndRoute(self.__WB_MANIFEST);

// Background Sync Queue for POST requests to API
const bgSyncPlugin = new BackgroundSyncPlugin('api-queue', {
  maxRetentionTime: 24 * 60, // Retain for 24 hours (in minutes)
  onSync: async ({ queue }) => {
    let entry;
    while ((entry = await queue.shiftRequest())) {
      try {
        await fetch(entry.request.clone());
        console.log('[SW] Replayed queued request:', entry.request.url);
      } catch (err) {
        // Put it back if it still fails
        await queue.unshiftRequest(entry);
        throw err;
      }
    }
  },
});

// Use NetworkOnly + BackgroundSync for mutation endpoints
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/') && url.method === 'POST',
  new NetworkOnly({ plugins: [bgSyncPlugin] }),
  'POST'
);

7. Push notifications with the Web Push API

Push notifications are the third pillar of a complete PWA alongside offline-first and installability. They allow reaching users even when the app is not open. Technically they rely on the Web Push Protocol: the browser creates a push subscription object that is sent to the server. The server sends encrypted messages through the browser vendor's push service, which delivers them to the service worker.

The implementation requires VAPID keys (Voluntary Application Server Identification), generated server-side. In the frontend, the subscription is requested with registration.pushManager.subscribe(), which requires user permission. In the service worker, the push event is handled and the notification is shown via self.registration.showNotification(). For React apps, a custom hook that manages subscription state and exposes it as a context is recommended, so components can react to the notification status in a targeted way.

8. Service worker updates and versioning

The hardest part of a PWA is update management. When a new service worker becomes available, it first waits in the waiting state until all tabs of the app are closed. That means users can run an outdated version for hours without noticing. The solution is a visible update notification in the UI: when a new service worker is detected, the app shows a toast or banner prompting the user to reload the page.

The skipWaiting() pattern activates the new service worker immediately, without waiting for tabs to close. That carries risk though: if several tabs are open and controlled by different service worker versions, inconsistencies can occur. The safer approach is to call skipWaiting() only on an explicit user action, via a postMessage to the waiting worker. That puts the control in the user's hands and prevents surprise reloads during a checkout or a form submission.

9. Caching strategies compared

Choosing the right caching strategy depends on two dimensions: how important is freshness, and how important is availability while offline? The following table maps the five Workbox strategies to these dimensions.

Strategy Offline availability Freshness Use case
Cache First High Low Fonts, icons, unchanging images
Network First Medium (fallback) High API responses, HTML pages
Stale While Revalidate High Medium (delayed) JS/CSS bundles, avatars
Network Only None High Payments, auth token refresh
Cache Only High None App shell after precaching

In practice, a complete PWA always combines several strategies. The most common mistake is using a single strategy for every request, usually Cache First for everything, leading to stale API data, or Network First for everything, which negates the offline benefit entirely. Workbox enables granular rules per URL pattern, so every asset type can be handled optimally.

Mironsoft

React PWA · Workbox · Offline-First Architecture

Ready to turn your React app into a full PWA?

We analyze your existing React app and implement an offline-first architecture with Workbox, including caching strategy, background sync and update management.

PWA audit

Lighthouse analysis and identification of offline weaknesses in your app

Workbox setup

Implementing suitable caching strategies, precaching and background sync

Update handling

Safe service worker update management and user notification

10. Summary

A genuine React PWA with Workbox consists of several interlocking parts: a correctly registered service worker, a caching strategy suited to each asset type, precaching of the app shell, and optional background sync for offline actions. Workbox abstracts the Cache API and makes strategies configurable without having to implement every detail manually. next-pwa integrates Workbox seamlessly into the Next.js build and handles precaching of all chunks automatically.

The decisive step is a deliberate decision per asset type: what must always be current, what may be cached, and what must also work offline? This differentiation, not the mere installation of a service worker, is what separates a genuine offline-first app from a normal web app with a manifest file. Update management and background sync round out the picture and ensure users always have a consistent experience.

React PWA with Workbox - The essentials at a glance

Caching strategy

Cache First for static assets, Network First for APIs, Stale While Revalidate for bundles, never a one-size-fits-all strategy.

Precaching

Precache the app shell and critical assets during SW install. Workbox manages content hashes and invalidates only changed files.

Background sync

Buffer offline actions in IndexedDB, replay on reconnect. workbox-background-sync provides the queue implementation.

Update management

Trigger skipWaiting only on user action. Signal new SW versions via CustomEvent and show them in the UI as an update banner.

11. FAQ: React PWA with Workbox and next-pwa

1Workbox vs. next-pwa?
Workbox is the SW library, next-pwa is the Next.js plugin that integrates Workbox into the build. next-pwa configures Workbox via next.config.js.
2Disable PWA in dev mode?
Yes, always. SW interferes with HMR and serves cached instead of fresh assets. next-pwa: disable: process.env.NODE_ENV === 'development'.
3What is Background Sync?
Offline actions are delayed until network is available. The browser fires the sync event and the SW replays the stored requests.
4Handling SW updates safely?
Trigger skipWaiting only on user action via postMessage. Automatic triggering can cause inconsistency with multiple open tabs.
5Right strategy for APIs?
Network First with cache fallback. Cache First for APIs leads to stale data. maxAgeSeconds limits the validity.
6Precaching vs. runtime caching?
Precaching during SW install for app shell assets. Runtime caching on the first request for dynamic content and routes.
7Testing offline mode?
Chrome DevTools → Network → Offline. Lighthouse for automated PWA checks. workbox-window for programmatic SW control.
8Limiting cache size?
ExpirationPlugin with maxEntries and maxAgeSeconds in every Workbox route. LRU principle removes the oldest entries automatically.
9VAPID keys for push?
Public/private key pair for server identification. Generate with web-push. Keep the private key stored securely server-side.
10Workbox without Next.js?
Yes. vite-plugin-pwa for Vite, workbox-webpack-plugin for Webpack. Write the SW directly with Workbox functions.