A complete guide to building Progressive Web Apps. Service Workers, Web App Manifest, offline strategies, push notifications and device installation.
Introduction to PWA¶
Progressive Web Apps (PWAs) combine the best of web and mobile applications. They provide native-like experiences while remaining web applications that work across all devices and platforms.
Key PWA characteristics: - Progressive - Work for every user, regardless of browser choice - Responsive - Fit any form factor: desktop, mobile, tablet - Offline functionality - Work offline or with poor connectivity - App-like - Feel like native apps with app-style interactions - Secure - Served via HTTPS to prevent tampering - Installable - Can be installed on the home screen
Core Technologies¶
1. Web App Manifest¶
The manifest file provides metadata about your application:
{
"name": "My PWA App",
"short_name": "PWA App",
"description": "A progressive web application",
"start_url": "/",
"display": "standalone",
"theme_color": "#2196F3",
"background_color": "#ffffff",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
2. Service Workers¶
Service Workers enable offline functionality and background sync:
// Register service worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => console.log('SW registered'))
.catch(error => console.log('SW registration failed'));
}
// sw.js - Service Worker
const CACHE_NAME = 'my-pwa-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/offline.html'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Return cached version or fetch from network
return response || fetch(event.request);
})
.catch(() => {
// Return offline page for navigation requests
if (event.request.destination === 'document') {
return caches.match('/offline.html');
}
})
);
});
3. Offline Strategies¶
Different caching strategies for different content types:
// Cache first - for static assets
self.addEventListener('fetch', event => {
if (event.request.url.includes('/assets/')) {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
}
});
// Network first - for API data
self.addEventListener('fetch', event => {
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request)
.then(response => {
// Cache successful responses
const responseClone = response.clone();
caches.open(CACHE_NAME)
.then(cache => cache.put(event.request, responseClone));
return response;
})
.catch(() => caches.match(event.request))
);
}
});
Installation and App-like Experience¶
Install Prompt¶
Handle the install prompt programmatically:
let deferredPrompt;
window.addEventListener('beforeinstallprompt', e => {
// Prevent Chrome 67 and earlier from showing prompt
e.preventDefault();
// Stash event for later trigger
deferredPrompt = e;
// Show install button
showInstallButton();
});
function showInstallPrompt() {
if (deferredPrompt) {
deferredPrompt.prompt();
deferredPrompt.userChoice.then(result => {
if (result.outcome === 'accepted') {
console.log('User accepted install');
}
deferredPrompt = null;
});
}
}
App Shell Architecture¶
Implement the app shell pattern for instant loading:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My PWA</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="manifest" href="/manifest.json">
<!-- Critical CSS inlined -->
<style>
.app-shell { /* minimal shell styles */ }
</style>
</head>
<body>
<div class="app-shell">
<header class="header">
<!-- App header -->
</header>
<main class="main">
<!-- Dynamic content loaded here -->
</main>
<nav class="navigation">
<!-- App navigation -->
</nav>
</div>
<script src="/js/app.js"></script>
</body>
</html>
Push Notifications¶
Implement push notifications for user engagement:
// Request permission
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
subscribeToNotifications();
}
});
// Subscribe to push notifications
function subscribeToNotifications() {
navigator.serviceWorker.ready.then(registration => {
return registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicVapidKey)
});
}).then(subscription => {
// Send subscription to server
fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
});
}
// Handle push events in service worker
self.addEventListener('push', event => {
const options = {
body: event.data.text(),
icon: '/icons/icon-192x192.png',
badge: '/icons/badge-72x72.png',
actions: [
{ action: 'open', title: 'Open App' },
{ action: 'close', title: 'Close' }
]
};
event.waitUntil(
self.registration.showNotification('PWA Notification', options)
);
});
Best Practices¶
- Start with App Shell - Load core UI instantly
- Cache Strategy - Use appropriate strategies for different content
- Offline Experience - Provide meaningful offline functionality
- Performance - Optimize for mobile networks
- Progressive Enhancement - Work without service workers
- Install Experience - Guide users through installation
PWAs provide a powerful way to deliver app-like experiences on the web platform, combining the reach of web with the engagement of native applications.