Progressive Web App Offline Capabilities in 2026
The progressive web app platform has reached remarkable maturity in 2026. With the Chromium-based browsers, Safari, and Firefox all supporting the core PWA APIs, developers can build applications that work offline, sync in the background, send push notifications, and access hardware features — all from a single codebase delivered through the web. The gap between native and web apps has never been narrower.
This guide covers building offline-first PWAs with modern service worker patterns, background synchronization, push notifications, and the latest Web APIs that make PWAs viable alternatives to native applications for many use cases.
Service Worker Architecture
The service worker is the backbone of any PWA. It acts as a programmable network proxy, intercepting fetch requests and serving cached responses when the network is unavailable. Moreover, modern service worker patterns go far beyond simple cache-first strategies:
// sw.js — Production service worker
const CACHE_VERSION = 'v3';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const DYNAMIC_CACHE = `dynamic-${CACHE_VERSION}`;
const API_CACHE = `api-${CACHE_VERSION}`;
const STATIC_ASSETS = [
'/',
'/index.html',
'/app.js',
'/styles.css',
'/manifest.json',
'/offline.html',
];
// Install: pre-cache static assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then(cache => cache.addAll(STATIC_ASSETS))
.then(() => self.skipWaiting())
);
});
// Activate: clean old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(
keys.filter(key => !key.includes(CACHE_VERSION))
.map(key => caches.delete(key))
)
).then(() => self.clients.claim())
);
});
// Fetch: strategy-based routing
self.addEventListener('fetch', event => {
const { request } = event;
const url = new URL(request.url);
// API calls: network-first with cache fallback
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirstWithCache(request));
return;
}
// Static assets: cache-first
if (STATIC_ASSETS.includes(url.pathname)) {
event.respondWith(cacheFirst(request));
return;
}
// Images: stale-while-revalidate
if (request.destination === 'image') {
event.respondWith(staleWhileRevalidate(request));
return;
}
// Everything else: network with offline fallback
event.respondWith(
fetch(request).catch(() => caches.match('/offline.html'))
);
});Caching Strategy Implementations
// Cache-first: for static assets that rarely change
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
const cache = await caches.open(STATIC_CACHE);
cache.put(request, response.clone());
return response;
}
// Network-first: for API data that needs freshness
async function networkFirstWithCache(request) {
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(API_CACHE);
cache.put(request, response.clone());
}
return response;
} catch (error) {
const cached = await caches.match(request);
if (cached) return cached;
return new Response(
JSON.stringify({ error: 'Offline', cached: false }),
{ headers: { 'Content-Type': 'application/json' } }
);
}
}
// Stale-while-revalidate: for content that can be slightly stale
async function staleWhileRevalidate(request) {
const cache = await caches.open(DYNAMIC_CACHE);
const cached = await cache.match(request);
const networkFetch = fetch(request).then(response => {
cache.put(request, response.clone());
return response;
});
return cached || networkFetch;
}Background Sync for Offline Actions
Background sync allows PWAs to defer actions when offline and execute them when connectivity returns. Therefore, users can continue working seamlessly without connectivity:
// In your app: queue offline actions
async function submitForm(data) {
try {
const response = await fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return response.json();
} catch (error) {
// Offline: save to IndexedDB and register sync
await saveToOutbox(data);
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('outbox-sync');
return { queued: true, message: 'Will sync when online' };
}
}
// In service worker: process queued actions
self.addEventListener('sync', event => {
if (event.tag === 'outbox-sync') {
event.waitUntil(processOutbox());
}
});
async function processOutbox() {
const db = await openDB('app-db', 1);
const items = await db.getAll('outbox');
for (const item of items) {
try {
await fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item.data),
});
await db.delete('outbox', item.id);
} catch (error) {
// Will retry on next sync event
break;
}
}
}Push Notifications
PWA push notifications now work across all major browsers. Additionally, they are the primary re-engagement tool for web applications:
// Request permission and subscribe
async function subscribeToPush() {
const permission = await Notification.requestPermission();
if (permission !== 'granted') return null;
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
});
// Send subscription to server
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription),
});
return subscription;
}
// Service worker: handle push events
self.addEventListener('push', event => {
const data = event.data?.json() || {};
event.waitUntil(
self.registration.showNotification(data.title || 'Update', {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
image: data.image,
actions: data.actions || [
{ action: 'open', title: 'Open App' },
{ action: 'dismiss', title: 'Dismiss' },
],
data: { url: data.url || '/' },
})
);
});
self.addEventListener('notificationclick', event => {
event.notification.close();
const url = event.notification.data.url;
event.waitUntil(
clients.matchAll({ type: 'window' }).then(windowClients => {
for (const client of windowClients) {
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
return clients.openWindow(url);
})
);
});Web App Manifest
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"start_url": "/?source=pwa",
"display": "standalone",
"background_color": "#0B1121",
"theme_color": "#3B82F6",
"orientation": "any",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/icons/maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
],
"screenshots": [
{ "src": "/screenshots/desktop.png", "sizes": "1280x720", "type": "image/png", "form_factor": "wide" },
{ "src": "/screenshots/mobile.png", "sizes": "750x1334", "type": "image/png", "form_factor": "narrow" }
],
"share_target": {
"action": "/share",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url"
}
}
}When NOT to Use PWA
PWAs cannot access all native device features. Bluetooth LE, NFC, and advanced camera controls are limited or unavailable on iOS. If your app requires these capabilities, native development remains necessary. Furthermore, PWAs on iOS still have storage limitations (the OS may evict service worker caches after 14 days of inactivity). For App Store distribution and monetization, native apps provide a smoother path. Consequently, evaluate whether your specific feature requirements and target platform support PWA capabilities before committing.
Key Takeaways
- A progressive web app in 2026 can handle offline work, push notifications, and background sync across all major browsers
- Strategy-based service worker routing (cache-first, network-first, stale-while-revalidate) optimizes both performance and freshness
- Background sync queues offline mutations and automatically processes them when connectivity returns
- Push notifications provide native-like re-engagement with VAPID key authentication
- Evaluate iOS limitations carefully — storage eviction and API gaps may impact your specific use case
Related Reading
- Astro 5 Content Layer and Hybrid Rendering
- Cloudflare Workers D1 Edge Database
- Bun 2 Runtime and Bundler Performance