Progressive Web Apps deliver native app experiences through the web. 2025 brings enhanced capabilities with better iOS support, advanced caching, and seamless installations. At ZIRA Software, PWAs reduced our mobile development costs by 50%.
PWA Capabilities 2025
PWA Feature Support
├── Installation (Add to Home Screen)
├── Offline functionality
├── Push notifications
├── Background sync
├── File system access
├── Bluetooth/USB access
├── Screen wake lock
├── Share target
└── Badging API
Platform Support:
├── Chrome/Edge: Full support
├── Safari/iOS: Much improved (2025)
├── Firefox: Good support
└── Samsung Internet: Full support
Service Worker Setup
// service-worker.ts
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
// Precache static assets
precacheAndRoute(self.__WB_MANIFEST);
cleanupOutdatedCaches();
// Cache strategies for different resources
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new ExpirationPlugin({
maxEntries: 100,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
}),
],
})
);
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api-cache',
plugins: [
new ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 5 * 60, // 5 minutes
}),
],
})
);
registerRoute(
({ request }) => request.destination === 'document',
new NetworkFirst({
cacheName: 'pages',
})
);
// Offline fallback
registerRoute(
({ request }) => request.mode === 'navigate',
async ({ event }) => {
try {
return await new NetworkFirst().handle({ event });
} catch {
return caches.match('/offline.html');
}
}
);
Web App Manifest
// manifest.json
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"description": "A powerful web application",
"start_url": "/",
"display": "standalone",
"orientation": "portrait-primary",
"theme_color": "#0ea5e9",
"background_color": "#ffffff",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"screenshots": [
{
"src": "/screenshots/home.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",
"files": [
{
"name": "media",
"accept": ["image/*", "video/*"]
}
]
}
},
"shortcuts": [
{
"name": "New Task",
"url": "/tasks/new",
"icons": [{ "src": "/icons/new-task.png", "sizes": "96x96" }]
}
]
}
Push Notifications
// lib/push-notifications.ts
export async function subscribeToPush(): Promise<PushSubscription | null> {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
return null;
}
const registration = await navigator.serviceWorker.ready;
// Check existing subscription
let subscription = await registration.pushManager.getSubscription();
if (!subscription) {
// Subscribe to push
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 push handler
// service-worker.ts
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? {};
const options: NotificationOptions = {
body: data.body,
icon: '/icons/notification.png',
badge: '/icons/badge.png',
image: data.image,
actions: data.actions,
data: data.url,
vibrate: [100, 50, 100],
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow(event.notification.data || '/')
);
});
Background Sync
// Register background sync
async function saveForLater(data: FormData) {
const registration = await navigator.serviceWorker.ready;
// Store data in IndexedDB
await idb.put('pending-submissions', {
id: Date.now(),
data: Object.fromEntries(data),
timestamp: new Date().toISOString(),
});
// Register sync
await registration.sync.register('sync-submissions');
}
// Service worker sync handler
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-submissions') {
event.waitUntil(syncPendingSubmissions());
}
});
async function syncPendingSubmissions() {
const pending = await idb.getAll('pending-submissions');
for (const item of pending) {
try {
await fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item.data),
});
await idb.delete('pending-submissions', item.id);
} catch {
// Will retry on next sync
}
}
}
Install Prompt
// hooks/usePWAInstall.ts
export function usePWAInstall() {
const [installPrompt, setInstallPrompt] = useState<BeforeInstallPromptEvent | null>(null);
const [isInstalled, setIsInstalled] = useState(false);
useEffect(() => {
// Check if already installed
if (window.matchMedia('(display-mode: standalone)').matches) {
setIsInstalled(true);
return;
}
const handler = (e: BeforeInstallPromptEvent) => {
e.preventDefault();
setInstallPrompt(e);
};
window.addEventListener('beforeinstallprompt', handler);
window.addEventListener('appinstalled', () => {
setIsInstalled(true);
setInstallPrompt(null);
});
return () => window.removeEventListener('beforeinstallprompt', handler);
}, []);
const install = async () => {
if (!installPrompt) return false;
installPrompt.prompt();
const result = await installPrompt.userChoice;
return result.outcome === 'accepted';
};
return { canInstall: !!installPrompt, isInstalled, install };
}
Conclusion
PWAs in 2025 deliver native-like experiences with offline support, push notifications, and seamless installation. Improved iOS support and new APIs make PWAs viable for most mobile use cases.
Building a PWA? Contact ZIRA Software for progressive web app development.