Progressive Web App Overview
Build progressive web applications with offline support, installability, service workers, and web app manifests to deliver app-like experiences in the browser.
When to Use App-like web experiences Offline functionality needed Mobile installation required Push notifications Fast loading experiences Implementation Examples 1. Web App Manifest // public/manifest.json { "name": "My Awesome App", "short_name": "AwesomeApp", "description": "A progressive web application", "start_url": "/", "scope": "/", "display": "standalone", "orientation": "portrait-primary", "background_color": "#ffffff", "theme_color": "#007bff", "icons": [ { "src": "/images/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { "src": "/images/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { "src": "/images/icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "/images/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ], "screenshots": [ { "src": "/images/screenshot-1.png", "sizes": "540x720", "type": "image/png", "form_factor": "narrow" }, { "src": "/images/screenshot-2.png", "sizes": "1280x720", "type": "image/png", "form_factor": "wide" } ], "categories": ["productivity", "utilities"], "shortcuts": [ { "name": "Quick Note", "short_name": "Note", "description": "Create a quick note", "url": "/new-note", "icons": [ { "src": "/images/note-icon.png", "sizes": "192x192" } ] } ] }
- Service Worker Implementation // public/service-worker.ts const CACHE_NAME = 'app-v1'; const STATIC_ASSETS = [ '/', '/index.html', '/css/main.css', '/js/app.js', '/images/icon-192.png', '/offline.html' ];
// Install event self.addEventListener('install', (event: ExtendableEvent) => { event.waitUntil( caches.open(CACHE_NAME).then(cache => { return cache.addAll(STATIC_ASSETS); }) ); self.skipWaiting(); });
// Activate event self.addEventListener('activate', (event: ExtendableEvent) => { event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames .filter(name => name !== CACHE_NAME) .map(name => caches.delete(name)) ); }) ); self.clients.claim(); });
// Fetch event with cache-first strategy for static assets self.addEventListener('fetch', (event: FetchEvent) => { const { request } = event;
// Skip non-GET requests if (request.method !== 'GET') { return; }
// Cache first for static assets if (request.destination === 'image' || request.destination === 'font') { event.respondWith( caches.match(request).then(response => { return response || fetch(request).then(res => { if (res.ok) { const clone = res.clone(); caches.open(CACHE_NAME).then(cache => { cache.put(request, clone); }); } return res; }); }).catch(() => { return caches.match('/offline.html'); }) ); }
// Network first for API calls if (request.url.includes('/api/')) { event.respondWith( fetch(request) .then(response => { if (response.ok) { const clone = response.clone(); caches.open(CACHE_NAME).then(cache => { cache.put(request, clone); }); } return response; }) .catch(() => { return caches.match(request); }) ); }
// Stale while revalidate for HTML if (request.destination === 'document') { event.respondWith( caches.match(request).then(cachedResponse => { const fetchPromise = fetch(request).then(response => { if (response.ok) { caches.open(CACHE_NAME).then(cache => { cache.put(request, response.clone()); }); } return response; });
return cachedResponse || fetchPromise;
})
);
} });
// Background Sync self.addEventListener('sync', (event: any) => { if (event.tag === 'sync-notes') { event.waitUntil(syncNotes()); } });
async function syncNotes() { const db = await openDB('notes'); const unsynced = await db.getAll('keyval', IDBKeyRange.bound('pending_', 'pending_\uffff'));
for (const item of unsynced) { try { await fetch('/api/notes', { method: 'POST', body: JSON.stringify(item.value) }); await db.delete('keyval', item.key); } catch (error) { console.error('Sync failed:', error); } } }
- Install Prompt and App Installation // hooks/useInstallPrompt.ts import { useState, useEffect } from 'react';
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise
export const useInstallPrompt = () => {
const [promptEvent, setPromptEvent] = useState
useEffect(() => { const handleBeforeInstallPrompt = (e: Event) => { e.preventDefault(); setPromptEvent(e as BeforeInstallPromptEvent); };
const handleAppInstalled = () => {
setIsInstalled(true);
setPromptEvent(null);
};
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
window.addEventListener('appinstalled', handleAppInstalled);
// Check if running as installed app
if (window.matchMedia('(display-mode: standalone)').matches) {
setIsInstalled(true);
}
// Check iOS
const isIOSDevice = /iPad|iPhone|iPod/.test(navigator.userAgent);
const isIOSApp = navigator.standalone === true;
if (isIOSDevice && !isIOSApp) {
setIsIOSInstalled(false);
}
return () => {
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
window.removeEventListener('appinstalled', handleAppInstalled);
};
}, []);
const installApp = async () => { if (promptEvent) { await promptEvent.prompt(); const { outcome } = await promptEvent.userChoice; if (outcome === 'accepted') { setIsInstalled(true); } setPromptEvent(null); } };
return { promptEvent, canInstall: promptEvent !== null, isInstalled, isIOSInstalled, installApp }; };
// components/InstallPrompt.tsx export const InstallPrompt: React.FC = () => { const { canInstall, isInstalled, installApp } = useInstallPrompt();
if (isInstalled || !canInstall) return null;
return (
Install App
Install our app for quick access and offline support
- Offline Support with IndexedDB // db/notesDB.ts import { openDB, DBSchema, IDBPDatabase } from 'idb';
interface Note { id: string; title: string; content: string; timestamp: number; synced: boolean; }
interface NotesDB extends DBSchema { notes: { key: string; value: Note; indexes: { 'by-timestamp': number; 'by-synced': boolean }; }; }
let db: IDBPDatabase
export async function initDB() {
db = await openDB
export async function addNote(note: Omit
export async function getNotes(): Promise
export async function getUnsyncedNotes(): Promise
export async function updateNote(id: string, updates: Partial
export async function markAsSynced(id: string) { await updateNote(id, { synced: true }); }
- Push Notifications // services/pushNotification.ts export async function subscribeToPushNotifications() { if (!('serviceWorker' in navigator) || !('PushManager' in window)) { console.log('Push notifications not supported'); return; }
try { const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: process.env.REACT_APP_VAPID_PUBLIC_KEY });
// Send subscription to server
await fetch('/api/push-subscription', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
return subscription;
} catch (error) { console.error('Push subscription failed:', error); } }
// service-worker.ts self.addEventListener('push', (event: PushEvent) => { const data = event.data?.json() ?? {}; const options: NotificationOptions = { title: data.title || 'New Notification', body: data.message || '', icon: '/images/icon-192.png', badge: '/images/badge-72.png', tag: data.tag || 'notification' };
event.waitUntil( self.registration.showNotification(options.title, options) ); });
self.addEventListener('notificationclick', (event: NotificationEvent) => { event.notification.close(); event.waitUntil( self.clients.matchAll({ type: 'window' }).then(clients => { if (clients.length > 0) { return clients[0].focus(); } return self.clients.openWindow('/'); }) ); });
Best Practices Implement service workers for offline support Create comprehensive web app manifest Use cache strategies appropriate for content type Provide offline fallback pages Test on various network conditions Optimize for slow 3G networks Include installation prompts Use IndexedDB for local storage Monitor sync status and connectivity Handle update notifications gracefully Resources Web.dev Progressive Web Apps Service Workers API Web App Manifest IndexedDB API Push API