The Case for Offline-First
Mobile network conditions are unpredictable. A user on a train loses connectivity mid-session. A client at a construction site has intermittent signal. An international traveler is avoiding roaming data costs. If your web application requires a network connection to function, you are delivering a degraded experience to a significant percentage of your users. Progressive Web Apps with offline-first architecture solve this, and the browser APIs available in 2025 make the implementation more approachable than ever.
An offline-first approach does not mean your app works without any network connection forever — it means the app optimistically assumes cached data is available, degrades gracefully when the network is unavailable, and syncs automatically when connectivity returns. The difference in user experience is substantial.
Service Worker Registration
The service worker is the engine of a PWA. It is a JavaScript file that runs in a separate thread, intercepts network requests, manages caches, and handles background operations. Register it early in your application bootstrap.
// In your main entry file
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registered:', registration.scope);
})
.catch(err => {
console.error('SW registration failed:', err);
});
});
}
I use the Workbox library from Google rather than writing service worker code by hand. Workbox provides battle-tested implementations of every caching strategy and handles the subtle edge cases that manual implementations miss — like cache versioning and stale entry cleanup.
Cache Strategies: Choosing the Right One
Different resources need different caching strategies. Understanding the tradeoffs is essential to a good offline experience.
Cache First serves from cache and only goes to the network if the resource is not cached. Use this for static assets — fonts, icons, logos — that change rarely. The asset is always served instantly, regardless of network conditions.
Network First tries the network and falls back to cache if the network fails. Use this for API data that should be fresh when possible but still usable when offline. Add a timeout — if the network does not respond in 3 seconds, fall back to cache rather than making the user wait.
Stale While Revalidate serves from cache immediately (fast response) while fetching a fresh version in the background to update the cache for the next request. This is my default for pages and HTML documents — the user always gets an instant response, and the next visit gets the latest version.
// sw.js using Workbox
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
// Static assets: Cache First
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 })],
})
);
// API data: Network First with fallback
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({ cacheName: 'api-cache', networkTimeoutSeconds: 3 })
);
// HTML pages: Stale While Revalidate
registerRoute(
({ request }) => request.destination === 'document',
new StaleWhileRevalidate({ cacheName: 'pages' })
);
Manifest.json Setup
The web app manifest enables the installability aspect of PWAs. At minimum you need a name, short_name, start_url, display mode, background_color, and icons in multiple sizes.
{
"name": "My Application",
"short_name": "MyApp",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3b82f6",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}
The maskable purpose on the 512px icon is important for Android — without it, the icon gets a white circle background on the home screen rather than filling the adaptive icon shape.
Background Sync and Push Notifications
Background Sync lets you defer actions taken while offline until connectivity returns. When a user submits a form while offline, store the payload in IndexedDB and register a sync event. The service worker fires the sync event when connectivity is restored and sends the stored payload.
Push notifications require server-side implementation using the Web Push Protocol. Generate VAPID keys, store the subscription object on your server when the user grants permission, and send notifications via the web-push npm library. The service worker handles incoming push events and displays notifications via the Notifications API even when the app is closed.
Install Prompt and Lighthouse Audit
The browser fires the beforeinstallprompt event when the PWA criteria are met. Capture it, show your own install UI at an appropriate moment (not immediately on page load), and call prompt() when the user clicks install. This gives you control over the install experience rather than relying on the browser's default behavior.
Run a Lighthouse PWA audit during development. The audit checks service worker registration, manifest validity, HTTPS, offline capability, and several other criteria. Achieving a full PWA score correlates strongly with a good offline experience. I run this check as part of my CI pipeline using lhci autorun to catch regressions before they reach production.
An offline-first PWA is not significantly more work than a conventional web app if you build the service worker strategy in from the start. The user experience improvement — especially on mobile and in poor network conditions — is one of the most impactful things you can add to any web application.