PWA in 2026: Service Workers, Offline-First, and Modern Install Experiences
Progressive Web Apps have matured significantly. In 2026, PWAs offer capabilities that rival native apps on both Android and iOS. With service workers, background sync, push notifications, and file system access, the line between web and native continues to blur.
The PWA Capability Landscape in 2026
Modern PWAs can access:
- File System Access API: Read/write files with user permission
- Web Bluetooth and USB: Connect to hardware devices
- Background Sync: Queue actions for when connectivity returns
- Periodic Background Sync: Regular data updates (Android)
- Web Push: Notifications even when app is closed
- Badging API: App icon badges
- Share Target API: Receive shared content from other apps
Web App Manifest Deep Dive
{
"name": "My Production PWA",
"short_name": "MyPWA",
"display": "standalone",
"display_override": ["window-controls-overlay", "standalone", "browser"],
"background_color": "#ffffff",
"theme_color": "#6366f1",
"screenshots": [
{
"src": "/screenshots/desktop.png",
"sizes": "1280x800",
"type": "image/png",
"form_factor": "wide",
"label": "Desktop view"
}
],
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
],
"file_handlers": [
{
"action": "/open-file",
"accept": { "text/markdown": [".md"] }
}
],
"share_target": {
"action": "/share-target",
"method": "POST",
"enctype": "multipart/form-data",
"params": { "title": "title", "text": "text", "url": "url" }
}
}
Advanced Service Worker Patterns
Workbox 7.x Configuration
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { BroadcastUpdatePlugin } from 'workbox-broadcast-update';
precacheAndRoute(self.__WB_MANIFEST);
cleanupOutdatedCaches();
// HTML - Network First
registerRoute(
({ request }) => request.destination === 'document',
new NetworkFirst({
cacheName: 'html-cache',
networkTimeoutSeconds: 3,
plugins: [new ExpirationPlugin({ maxAgeSeconds: 24 * 60 * 60 })],
})
);
// API - Network First with update notification
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api-cache',
networkTimeoutSeconds: 5,
plugins: [
new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 5 * 60 }),
new BroadcastUpdatePlugin(),
],
})
);
// Images - Cache First, long expiry
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'image-cache',
plugins: [
new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 }),
],
})
);
Background Sync
const bgSyncPlugin = new BackgroundSyncPlugin('form-queue', {
maxRetentionTime: 24 * 60,
onSync: async ({ queue }) => {
let entry;
while ((entry = await queue.shiftRequest())) {
try {
await fetch(entry.request);
} catch (error) {
await queue.unshiftRequest(entry);
throw error;
}
}
},
});
registerRoute(
({ url }) => url.pathname === '/api/submit',
new NetworkOnly({ plugins: [bgSyncPlugin] }),
'POST'
);
Smart Update Management
class ServiceWorkerManager {
async register() {
if (!('serviceWorker' in navigator)) return;
this.registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/',
updateViaCache: 'none',
});
setInterval(() => this.registration.update(), 60 * 60 * 1000);
this.registration.addEventListener('updatefound', () => {
const newWorker = this.registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
this.onUpdateAvailable?.();
}
});
});
navigator.serviceWorker.addEventListener('controllerchange', () => {
window.location.reload();
});
}
skipWaiting() {
this.registration?.waiting?.postMessage({ type: 'SKIP_WAITING' });
}
}
Push Notifications
Requesting Permission and Subscribing
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready;
const permission = await Notification.requestPermission();
if (permission !== 'granted') throw new Error('Permission denied');
const sub = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY),
});
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sub.toJSON()),
});
return sub;
}
Handling Push in Service Worker
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? {};
event.waitUntil(
self.registration.showNotification(data.title ?? 'New Update', {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
data: { url: data.url },
actions: [
{ action: 'open', title: 'Open App' },
{ action: 'dismiss', title: 'Dismiss' },
],
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'dismiss') return;
const url = event.notification.data?.url ?? '/';
event.waitUntil(
clients.matchAll({ type: 'window' }).then((clientList) => {
const existing = clientList.find(c => c.url === url);
if (existing) return existing.focus();
return clients.openWindow(url);
})
);
});
Custom Install Prompt
class PWAInstallManager {
private deferredPrompt = null;
constructor() {
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
this.deferredPrompt = e;
this.onInstallAvailable?.();
});
window.addEventListener('appinstalled', () => {
this.deferredPrompt = null;
analytics.track('pwa_installed');
});
}
get canInstall() { return !!this.deferredPrompt; }
async promptInstall() {
if (!this.deferredPrompt) return null;
this.deferredPrompt.prompt();
const { outcome } = await this.deferredPrompt.userChoice;
this.deferredPrompt = null;
return outcome;
}
}
Periodic Background Sync
// Register periodic sync
async function registerPeriodicSync() {
const registration = await navigator.serviceWorker.ready;
if ('periodicSync' in registration) {
try {
await registration.periodicSync.register('update-feed', {
minInterval: 24 * 60 * 60 * 1000, // 24 hours
});
} catch (e) {
console.log('Periodic sync registration failed:', e);
}
}
}
// Handle in service worker
self.addEventListener('periodicsync', (event) => {
if (event.tag === 'update-feed') {
event.waitUntil(updateFeedCache());
}
});
Conclusion
PWAs in 2026 are genuinely capable cross-platform applications. With advanced Service Worker strategies, background sync, push notifications, and thoughtful install experiences, you can deliver app-store-quality experiences through the web. The investment in PWA architecture pays dividends across all platforms simultaneously.