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.
Table of Contents
- 1. What sets a genuine PWA apart from a normal web app
- 2. Service worker: lifecycle and registration
- 3. Workbox: caching strategies in detail
- 4. next-pwa: integration into Next.js projects
- 5. Precaching: securing the app shell and critical assets
- 6. Background sync: replaying offline actions
- 7. Push notifications with the Web Push API
- 8. Service worker updates and versioning
- 9. Caching strategies compared
- 10. Summary
- 11. FAQ
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.