Building Production-Ready PWAs with Workbox in 2026
Complete guide to Progressive Web Apps with Workbox in 2026. Master Service Worker lifecycle, caching strategies, offline-first patterns, Web App Manifest, and push notifications. Real code examples included.
Progressive Web Apps have a reputation for being complicated to get right. The theory sounds simple β a web app that works offline, installs on the home screen, and feels native β but implementing it correctly involves Service Worker lifecycle subtleties, cache invalidation pitfalls, and manifest quirks that can turn a weekend project into a debugging marathon.
Workbox is Googleβs answer to that complexity. It abstracts the hard parts of Service Workers into a high-level API with battle-tested defaults, while staying flexible enough for advanced use cases. In 2026, Workbox v7 is stable, well-documented, and used by some of the largest web properties on the planet.
This guide covers everything: fundamentals, Service Worker lifecycle, every major Workbox strategy, offline-first patterns, and a complete working example.
PWA Fundamentals
A Progressive Web App is defined by three core requirements:
- HTTPS β Service Workers only register on secure origins (localhost is the exception)
- Service Worker β a JavaScript file running in a background thread, intercepting network requests
- Web App Manifest β a JSON file describing how the app appears when installed
Beyond the minimum, production PWAs also add push notifications, background sync, and periodic background sync. Letβs start from the foundation.
What Makes a Good PWA?
Chromeβs PWA criteria (required for install prompts):
- Served over HTTPS
- Has a valid manifest with
name,icons(192px + 512px),start_url,display: standalone - Has a registered Service Worker
Lighthouseβs full PWA audit goes further β offline support, reasonable response times, meta viewport, and more. A passing Lighthouse PWA score of 100 is a solid target.
Service Worker Lifecycle
The Service Worker lifecycle is the source of most PWA confusion. Understanding it removes the mystery.
States
ββββββββββββ
β Parsed β (downloaded, evaluated)
ββββββ¬ββββββ
β install event
ββββββΌββββββ
β Installingβ
ββββββ¬ββββββ
β activate event
ββββββΌββββββ
fetch events βββ Active βββ idle
ββββββ¬ββββββ
β new SW detected
ββββββΌββββββ
β Waiting β (new SW waiting for old to release)
ββββββββββββ
The Waiting Problem
When you deploy a new Service Worker, the old one keeps running until all tabs using it are closed. This is safe but annoying β users on old tabs block the update.
The fix: skipWaiting() + clients.claim():
// sw.js
self.addEventListener('install', (event) => {
self.skipWaiting(); // Activate immediately
});
self.addEventListener('activate', (event) => {
event.waitUntil(clients.claim()); // Take control of all pages
});
Use skipWaiting for non-critical updates. For breaking changes, show a βNew version available β reloadβ banner instead.
Registration
// main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/'
});
console.log('SW registered:', registration.scope);
} catch (err) {
console.error('SW registration failed:', err);
}
});
}
Always register after load β donβt block initial page render.
Introduction to Workbox
Workbox is a set of JavaScript libraries for adding offline support to web apps. At its core, it provides:
- Routing β match requests by URL, type, or custom logic
- Strategies β pre-built caching patterns (cache-first, network-first, etc.)
- Precaching β cache-bust static assets at build time
- Expiration β limit cache size and age automatically
- Background Sync β retry failed requests when back online
- Broadcast Cache Update β notify pages when cached assets update
Installation
npm install workbox-window workbox-routing workbox-strategies \
workbox-precaching workbox-expiration workbox-background-sync
Or use the CDN in the Service Worker:
// sw.js β CDN approach (simpler, slightly larger)
importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.3.0/workbox-sw.js');
For production, use npm + a bundler. For prototyping, the CDN is fine.
Workbox Caching Strategies
Workbox ships five caching strategies. Choosing the right one for each resource type is the key skill.
1. CacheFirst
Check cache first. If found, return it. If not, fetch and cache.
Use for: Static assets that change infrequently β fonts, images, versioned JS/CSS.
import { registerRoute } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images-v1',
plugins: [
new ExpirationPlugin({
maxEntries: 60, // Keep only 60 images
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
}),
],
})
);
Risk: Users see stale content if you donβt version your assets. Always use content hashing in filenames (Webpack/Vite do this automatically).
2. NetworkFirst
Try the network first. On failure, fall back to cache.
Use for: HTML pages, API responses β content that should be fresh but must work offline.
import { NetworkFirst } from 'workbox-strategies';
registerRoute(
({ request }) => request.mode === 'navigate',
new NetworkFirst({
cacheName: 'pages-v1',
networkTimeoutSeconds: 3, // Fall back to cache after 3s
plugins: [
new ExpirationPlugin({ maxEntries: 25 }),
],
})
);
3. StaleWhileRevalidate
Return cached version immediately. Simultaneously fetch from network and update cache.
Use for: Non-critical assets where freshness matters but speed matters more β avatars, non-blocking scripts, CSS.
import { StaleWhileRevalidate } from 'workbox-strategies';
registerRoute(
({ url }) => url.pathname.startsWith('/api/feed'),
new StaleWhileRevalidate({
cacheName: 'api-feed-v1',
plugins: [
new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 3600 }),
],
})
);
4. NetworkOnly
Always fetch from network, never cache. Falls through to browser default.
Use for: Analytics pings, payment APIs, anything where caching is dangerous.
import { NetworkOnly } from 'workbox-strategies';
registerRoute(
({ url }) => url.pathname.startsWith('/api/checkout'),
new NetworkOnly()
);
5. CacheOnly
Only serve from cache, never network. Fails if not cached.
Use for: Rarely used alone. Useful when youβve pre-cached everything needed for offline and want strict cache enforcement.
import { CacheOnly } from 'workbox-strategies';
registerRoute(
({ url }) => url.pathname.startsWith('/offline-assets/'),
new CacheOnly({ cacheName: 'offline-essential-v1' })
);
Strategy Decision Matrix
| Resource Type | Strategy | Why |
|---|---|---|
| HTML pages | NetworkFirst | Fresh by default, fallback for offline |
| Versioned JS/CSS | CacheFirst | Immutable assets (content-hashed) |
| Google Fonts | CacheFirst + ExpirationPlugin | Rarely change, large bandwidth |
| API data (feed) | StaleWhileRevalidate | Speed + eventual freshness |
| API data (critical) | NetworkFirst | Must be current |
| User avatar images | StaleWhileRevalidate | Good enough if slightly stale |
| Payment/auth APIs | NetworkOnly | Never cache sensitive ops |
Precaching with Workbox
Precaching downloads and caches assets at install time, before any user navigates to them. Itβs the foundation of reliable offline experiences.
Vite Integration
npm install -D vite-plugin-pwa
// vite.config.js
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.ico', 'robots.txt', 'apple-touch-icon.png'],
manifest: {
name: 'My App',
short_name: 'MyApp',
description: 'My Progressive Web App',
theme_color: '#ffffff',
icons: [
{ src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' },
],
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.myapp\.com\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: { maxEntries: 100, maxAgeSeconds: 3600 },
},
},
],
},
}),
],
});
Webpack Integration
npm install -D workbox-webpack-plugin
// webpack.config.js
const { InjectManifest } = require('workbox-webpack-plugin');
module.exports = {
plugins: [
new InjectManifest({
swSrc: './src/sw.js', // Your Service Worker source
swDest: 'sw.js', // Output name
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5MB limit
}),
],
};
// src/sw.js
import { precacheAndRoute } from 'workbox-precaching';
// __WB_MANIFEST is replaced by Webpack with the asset manifest
precacheAndRoute(self.__WB_MANIFEST);
The build tool injects the asset manifest with content hashes into your SW at build time. On deploy, only changed files get re-downloaded.
Web App Manifest
The manifest controls how your app looks when installed. Hereβs a production-ready example:
{
"name": "DevPlaybook",
"short_name": "DevPlaybook",
"description": "Developer tools and guides",
"start_url": "/?source=pwa",
"scope": "/",
"display": "standalone",
"orientation": "any",
"theme_color": "#1a1a2e",
"background_color": "#ffffff",
"lang": "en",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"screenshots": [
{
"src": "/screenshots/desktop.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide",
"label": "Desktop view"
}
],
"shortcuts": [
{
"name": "JSON Formatter",
"url": "/tools/json-formatter",
"icons": [{ "src": "/icons/shortcut-json.png", "sizes": "96x96" }]
}
]
}
Key fields:
start_urlβ add?source=pwafor analyticsdisplay: standaloneβ hides browser UI (required for install prompt)purpose: maskableβ for adaptive icons on Android (use Maskable.app to generate)shortcutsβ home screen quick-actions (Android/Windows)screenshotsβ shown in install dialogs on some platforms
Offline-First Patterns
Offline Fallback Page
Show a custom page when navigation fails and thereβs nothing cached:
// sw.js
import { setCatchHandler, setDefaultHandler } from 'workbox-routing';
import { NetworkOnly, NetworkFirst } from 'workbox-strategies';
import { precacheAndRoute } from 'workbox-precaching';
precacheAndRoute([
{ url: '/offline.html', revision: '1' },
...self.__WB_MANIFEST,
]);
// Default: NetworkFirst for navigation
setDefaultHandler(new NetworkFirst());
// Catch-all for navigation failures
setCatchHandler(async ({ event }) => {
if (event.request.destination === 'document') {
return caches.match('/offline.html');
}
return Response.error();
});
Background Sync for Form Submissions
Queue failed POST requests and retry when back online:
import { BackgroundSyncPlugin } from 'workbox-background-sync';
import { registerRoute } from 'workbox-routing';
import { NetworkOnly } from 'workbox-strategies';
const bgSyncPlugin = new BackgroundSyncPlugin('form-queue', {
maxRetentionTime: 24 * 60, // 24 hours in minutes
});
registerRoute(
({ url }) => url.pathname === '/api/submit',
new NetworkOnly({ plugins: [bgSyncPlugin] }),
'POST'
);
When the user submits a form offline, it queues to IndexedDB. When connectivity returns, the browser fires the sync event and Workbox replays the request automatically.
Broadcast Cache Update
Notify the page when a cached resource has been updated in the background:
// sw.js
import { StaleWhileRevalidate } from 'workbox-strategies';
import { BroadcastUpdatePlugin } from 'workbox-broadcast-update';
import { registerRoute } from 'workbox-routing';
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new StaleWhileRevalidate({
plugins: [new BroadcastUpdatePlugin()],
})
);
// main.js β listen for updates
const channel = new BroadcastChannel('workbox');
channel.addEventListener('message', (event) => {
if (event.data.type === 'CACHE_UPDATED') {
showToast('New content available β reload to update');
}
});
Push Notifications
Push notifications require a push subscription, a server with VAPID keys, and a Service Worker to receive pushes.
Step 1: Generate VAPID Keys
npx web-push generate-vapid-keys
Step 2: Subscribe the User
// main.js
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
});
// Send subscription to your server
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
headers: { 'Content-Type': 'application/json' },
});
}
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = atob(base64);
return Uint8Array.from([...rawData].map((c) => c.charCodeAt(0)));
}
Step 3: Handle Push in Service Worker
// sw.js
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? { title: 'Update', body: 'New content' };
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
data: { url: data.url },
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow(event.notification.data.url || '/')
);
});
Workbox with React / Next.js
next-pwa (Next.js)
npm install next-pwa
// next.config.js
const withPWA = require('next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development',
runtimeCaching: require('./cache.config.js'),
});
module.exports = withPWA({ /* next config */ });
// cache.config.js
module.exports = [
{
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts-stylesheets',
expiration: { maxEntries: 4, maxAgeSeconds: 365 * 24 * 60 * 60 },
},
},
// ... more rules
];
Update Prompt with workbox-window
// app.js
import { Workbox } from 'workbox-window';
const wb = new Workbox('/sw.js');
wb.addEventListener('waiting', () => {
// New SW waiting β prompt user
if (confirm('New version available! Update now?')) {
wb.messageSkipWaiting();
wb.addEventListener('controlling', () => window.location.reload());
}
});
wb.register();
Testing Your PWA
Lighthouse Audit
npx lighthouse https://yourapp.com --view --preset pwa
A passing PWA audit requires:
- β Registers a Service Worker
- β Works offline (200 response when offline)
- β Installable (manifest + icons + SW)
- β Uses HTTPS
DevTools
- Chrome DevTools β Application tab β Service Workers
- Check βOfflineβ checkbox to simulate network failure
- Cache Storage to inspect whatβs cached
- Manifest to validate your
manifest.json
Common Issues
| Issue | Fix |
|---|---|
| SW not updating | Check skipWaiting + clients.claim() |
| Assets not cached | Check globPatterns in Workbox config |
| Install prompt not showing | Verify manifest has all required fields |
| Push not working | Check VAPID keys match + subscription is fresh |
| Offline page not showing | Ensure /offline.html is precached |
Production Checklist
- HTTPS configured (or localhost for dev)
-
manifest.jsonhas name, icons (192+512), start_url, display: standalone - Maskable icon variant generated (Maskable.app)
- Service Worker registered after
loadevent - Precaching configured for critical assets
- Per-resource caching strategies tuned (see matrix above)
- Offline fallback page precached and wired up
- Background sync for POST requests
- Update prompt for new SW versions
- Lighthouse PWA score β₯ 90
- Tested on real Android device (install prompt behavior differs by OS)
Summary
PWAs in 2026 are a mature technology with clear implementation patterns. Workbox removes the tedious parts β cache busting, precache manifest generation, expiration policies β and lets you focus on which strategy fits each resource type.
The key decisions:
- Precache your app shell (HTML, CSS, JS) for instant loads
- NetworkFirst for navigations (fresh content, offline fallback)
- CacheFirst for versioned static assets (they never change)
- StaleWhileRevalidate for content where speed beats freshness
- Background Sync for form submissions and mutations
Start with vite-plugin-pwa or next-pwa, get a Lighthouse 100, then layer in push notifications and background sync as your users demand it. The install prompt and offline experience alone will improve retention metrics β users who install a PWA visit twice as often as browser-only users.
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links β we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.