Progressive Web Apps (PWAs)
Progressive Web Apps (PWAs) combine the best of web and mobile apps. They use modern web capabilities to deliver app-like experiences to users, with the reach and accessibility of the web.
PWA Architecture
Web App + Native-like Features = Progressive Web App
Core Principles of PWAs
Progressive Web Apps are built on three fundamental principles:
1. Reliable
Load instantly and never show the "dinosaur" offline page, even in uncertain network conditions.
2. Fast
Respond quickly to user interactions with silky smooth animations and scrolling.
3. Engaging
Feel like a natural app on the device, with an immersive user experience.
Key Technologies
1. Service Workers
Service Workers are JavaScript files that run separately from the main browser thread, intercepting network requests, caching resources, and enabling offline functionality.
// Register a service worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('Service Worker registered with scope:', registration.scope);
})
.catch(error => {
console.error('Service Worker registration failed:', error);
});
});
}
Basic Service Worker Lifecycle
- Registration: The browser registers the service worker script.
- Installation: The service worker installs and typically caches static assets.
- Activation: The service worker activates and can control pages.
- Idle: The service worker goes idle when not in use.
- Termination: The browser may terminate the service worker to conserve memory.
// service-worker.js
const CACHE_NAME = 'my-pwa-cache-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png'
];
// Installation - Cache static assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
// Fetch - Serve from cache, falling back to network
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Cache hit - return the response from the cached version
if (response) {
return response;
}
// Not in cache - fetch from network
return fetch(event.request)
.then(response => {
// Check if we received a valid response
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Clone the response
const responseToCache = response.clone();
// Add to cache
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
Note: Service Workers can only be used on websites served over HTTPS (except for localhost during development).
2. Web App Manifest
The Web App Manifest is a JSON file that provides information about a web application, allowing it to be installed on the home screen of a device.
// manifest.json
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"description": "An example Progressive Web App",
"start_url": "/index.html",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#4285f4",
"icons": [
{
"src": "/images/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/images/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
Link the manifest in your HTML:
<link rel="manifest" href="/manifest.json">
Key Manifest Properties
- name: The full name of the application.
- short_name: A shorter name for the app (used on home screens).
- start_url: The URL that loads when the app is launched.
- display: How the app should be displayed (fullscreen, standalone, minimal-ui, browser).
- background_color: The background color of the splash screen.
- theme_color: The color of the browser UI elements.
- icons: Array of icon objects with src, sizes, and type properties.
3. HTTPS
PWAs require secure connections. Service Workers only work on secure origins (HTTPS or localhost).
Tip: During development, you can use localhost. For production, always use HTTPS. Services like Let's Encrypt provide free SSL certificates.
Advanced PWA Features
Offline Capabilities
PWAs can work offline or with poor network connectivity using various caching strategies:
Strategy | Description | Best For |
---|---|---|
Cache First | Check cache first, fall back to network | Static assets that rarely change |
Network First | Try network first, fall back to cache | Content that updates frequently |
Stale While Revalidate | Return from cache, then update cache from network | Balance of performance and freshness |
Cache Only | Only serve from cache | Assets that never change |
Network Only | Only serve from network | Dynamic content that must be fresh |
Push Notifications
PWAs can send push notifications to users, even when the browser is closed.
// Request permission for notifications
function requestNotificationPermission() {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
console.log('Notification permission granted!');
// Subscribe the user to push notifications
subscribeToPushNotifications();
}
});
}
// Subscribe to push notifications
function subscribeToPushNotifications() {
navigator.serviceWorker.ready
.then(registration => {
// Check for push subscription
return registration.pushManager.getSubscription()
.then(subscription => {
if (subscription) {
return subscription;
}
// Get the server's public key
return fetch('/api/get-push-public-key')
.then(response => response.json())
.then(data => {
const publicKey = data.publicKey;
// Subscribe the user
return registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey)
});
});
})
.then(subscription => {
// Send the subscription to your server
return fetch('/api/save-subscription', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
});
})
.then(response => {
if (response.ok) {
console.log('User subscribed to push notifications!');
}
});
});
}
// Handle incoming push notifications in the service worker
// In service-worker.js
self.addEventListener('push', event => {
const data = event.data.json();
const options = {
body: data.body,
icon: '/images/notification-icon.png',
badge: '/images/badge-icon.png',
data: {
url: data.url
}
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
Background Sync
Background Sync allows web apps to defer actions until the user has stable connectivity.
// Register for background sync
function registerBackgroundSync() {
navigator.serviceWorker.ready
.then(registration => {
return registration.sync.register('send-messages');
})
.then(() => {
console.log('Background sync registered!');
})
.catch(err => {
console.error('Background sync registration failed:', err);
});
}
// In service-worker.js
self.addEventListener('sync', event => {
if (event.tag === 'send-messages') {
event.waitUntil(sendPendingMessages());
}
});
// Function to send pending messages
function sendPendingMessages() {
return fetch('/api/messages')
.then(response => response.json())
.then(data => {
const promises = data.pendingMessages.map(message => {
return fetch('/api/send-message', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(message)
});
});
return Promise.all(promises);
});
}
App Install Prompt
PWAs can prompt users to install the app on their device.
let deferredPrompt;
// Listen for the beforeinstallprompt event
window.addEventListener('beforeinstallprompt', event => {
// Prevent the default prompt
event.preventDefault();
// Store the event for later use
deferredPrompt = event;
// Show your custom install button
document.getElementById('install-button').style.display = 'block';
});
// Handle the install button click
document.getElementById('install-button').addEventListener('click', () => {
// Hide the button
document.getElementById('install-button').style.display = 'none';
// Show the prompt
deferredPrompt.prompt();
// Wait for the user's choice
deferredPrompt.userChoice.then(choiceResult => {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the install prompt');
} else {
console.log('User dismissed the install prompt');
}
// Clear the deferred prompt
deferredPrompt = null;
});
});
Testing and Auditing PWAs
Lighthouse
Lighthouse is an open-source tool from Google that audits web apps for performance, accessibility, and PWA features.
How to use Lighthouse:
- Open Chrome DevTools (F12 or Right-click > Inspect)
- Go to the "Lighthouse" tab
- Select "Progressive Web App" category
- Click "Generate report"
Workbox
Workbox is a set of libraries and tools that simplify the implementation of service workers and caching strategies.
// Using Workbox in service-worker.js
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js');
// Cache the Google Fonts stylesheets
workbox.routing.registerRoute(
({url}) => url.origin === 'https://fonts.googleapis.com',
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'google-fonts-stylesheets',
})
);
// Cache the Google Fonts webfont files
workbox.routing.registerRoute(
({url}) => url.origin === 'https://fonts.gstatic.com',
new workbox.strategies.CacheFirst({
cacheName: 'google-fonts-webfonts',
plugins: [
new workbox.cacheableResponse.CacheableResponsePlugin({
statuses: [0, 200],
}),
new workbox.expiration.ExpirationPlugin({
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
maxEntries: 30,
}),
],
})
);
// Cache CSS and JavaScript files
workbox.routing.registerRoute(
({request}) => request.destination === 'style' || request.destination === 'script',
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'static-resources',
})
);
// Cache images
workbox.routing.registerRoute(
({request}) => request.destination === 'image',
new workbox.strategies.CacheFirst({
cacheName: 'images',
plugins: [
new workbox.expiration.ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
}),
],
})
);
PWA Best Practices
- Responsive Design: Ensure your app works well on all device sizes.
- App Shell Architecture: Implement an app shell that loads instantly, then loads content dynamically.
- Offline First: Design with offline functionality in mind from the beginning.
- Performance Optimization: Minimize file sizes, reduce network requests, and optimize loading.
- Progressive Enhancement: Start with a basic experience that works everywhere, then enhance for modern browsers.
- Cross-Browser Testing: Test your PWA across different browsers and devices.
- Accessibility: Ensure your PWA is accessible to all users, including those with disabilities.
Warning: Don't cache everything! Be strategic about what you cache. Sensitive or frequently changing data should typically be fetched from the network.
Real-World PWA Examples
- Twitter Lite: A PWA version of Twitter that's fast and data-efficient.
- Starbucks: Their PWA allows ordering and payment, even offline.
- Pinterest: Their PWA led to significant increases in engagement and ad revenue.
- Spotify: Their PWA provides a lightweight alternative to the native app.
- Uber: Their PWA works across various network conditions and devices.
Next Steps
Now that you understand Progressive Web Apps, you can explore:
- Advanced caching strategies for complex applications
- Implementing offline analytics
- Creating app-like animations and transitions
- Using IndexedDB for client-side data storage
- Implementing PWA features in existing web applications