Progressive Web Apps Tutorial
Imagine a web page that feels just as snappy as a native app, works offline, and can be installed on a user’s home screen with a single tap. That’s the promise of Progressive Web Apps (PWAs). In this tutorial we’ll demystify the core building blocks, walk through a complete, working example, and explore real‑world scenarios where PWAs shine. By the end, you’ll be ready to turn any modern web project into a fast, reliable, and engaging experience.
What Makes a Web App “Progressive”?
A Progressive Web App is essentially a regular web application that meets a set of enhanced criteria. First, it must be served over HTTPS to guarantee security and enable service workers. Second, it needs a Web App Manifest that tells the browser how to display the app when installed. Finally, a service worker must be present to handle caching, background sync, and push notifications. When these pieces come together, the browser can treat the site like a native app—offline, installable, and performant.
These three pillars—security, manifest, and service worker—are not optional; they’re the minimum “progressive” requirements. Anything less still works as a normal website, but you won’t reap the full benefits of the PWA ecosystem.
Why Invest in a PWA?
- Offline capability: Users can continue browsing even without a network connection.
- App‑like feel: Smooth animations, responsive UI, and native‑style navigation.
- Discoverability: PWAs are indexed by search engines and can appear in app stores via Trusted Web Activity.
- Lower development cost: One codebase serves both web and mobile platforms.
Setting Up a Basic PWA
Let’s start with a minimal HTML page and gradually add the PWA ingredients. Create a new folder called pwa-demo and inside it place index.html, manifest.json, and service-worker.js. Below is the complete index.html file.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple PWA Demo</title>
<link rel="manifest" href="manifest.json">
<style>
body {font-family: Arial, sans-serif; padding: 2rem;}
.offline {color: red; font-weight: bold;}
</style>
</head>
<body>
<h1>Hello, Progressive Web App!</h1>
<p id="status">Checking connectivity...</p>
<script>
// Register the service worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js')
.then(() => console.log('Service Worker registered'))
.catch(err => console.error('SW registration failed:', err));
}
// Simple online/offline indicator
const statusEl = document.getElementById('status');
function updateStatus() {
if (navigator.onLine) {
statusEl.textContent = 'You are online';
statusEl.classList.remove('offline');
} else {
statusEl.textContent = 'You are offline';
statusEl.classList.add('offline');
}
}
window.addEventListener('online', updateStatus);
window.addEventListener('offline', updateStatus);
updateStatus();
</script>
</body>
</html>
This page does three things: it links to a manifest, registers a service worker, and shows an online/offline indicator. The next step is to define the manifest.
Creating the Web App Manifest
The manifest is a simple JSON file that describes how the app should appear when installed. Save the following as manifest.json in the same folder.
{
"name": "Simple PWA Demo",
"short_name": "PWA Demo",
"start_url": ".",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#4a90e2",
"icons": [
{
"src": "icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
Notice the display property set to standalone. This tells the browser to hide the URL bar and other UI chrome, giving the app a native feel. The icons array provides the images the OS will use on the home screen.
Adding a Service Worker for Offline Caching
The service worker is the heart of offline support. It intercepts network requests and decides whether to serve a cached response or fetch from the network. Below is a concise but functional service-worker.js that caches the core assets during the install phase and serves them from the cache on subsequent visits.
// Name of the cache
const CACHE_NAME = 'pwa-demo-v1';
const ASSETS_TO_CACHE = [
'/',
'/index.html',
'/manifest.json',
'/icon-192.png',
'/icon-512.png',
'/style.css' // optional, if you have a separate stylesheet
];
// Install event – cache essential files
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(ASSETS_TO_CACHE);
})
);
});
// Activate event – clean up old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys => {
return Promise.all(
keys.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
);
})
);
});
// Fetch event – serve from cache, fall back to network
self.addEventListener('fetch', event => {
if (event.request.method !== 'GET') return; // ignore non‑GET requests
event.respondWith(
caches.match(event.request).then(cachedResponse => {
return cachedResponse || fetch(event.request).then(networkResponse => {
// Optional: cache new requests dynamically
return caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
});
})
);
});
With these three files in place, you now have a functional PWA that works offline, can be added to the home screen, and displays a native‑like UI. To test locally, serve the folder over HTTPS (e.g., using http-server with the --ssl flag) and open the site in Chrome. The “Add to Home screen” prompt should appear after a few visits.
Real‑World Use Cases for PWAs
PWAs are not just a curiosity; they solve concrete problems across industries. News sites use PWAs to deliver fast, data‑light experiences for readers on slow connections. E‑commerce platforms employ them to keep shoppers engaged even when network quality drops, reducing cart abandonment. Even internal enterprise tools benefit from offline capabilities, allowing field workers to fill out forms without constant connectivity.
Let’s look at three concrete examples:
- Media & Publishing: The Washington Post’s PWA loads in under a second on 3G, dramatically improving user retention.
- Retail & Shopping: Alibaba reported a 76% increase in conversions after launching a PWA for its mobile shoppers.
- Travel & Booking: Trivago’s PWA enables users to browse hotel listings offline, boosting session length by 68%.
These successes illustrate how PWAs can bridge the gap between web reach and app‑like performance, delivering measurable business value.
Advanced Feature: Push Notifications
Push notifications turn a passive website into an active engagement channel. To enable them, you’ll need two pieces: a service worker that listens for push events, and a server endpoint that sends messages via the Web Push protocol. Below is a minimal addition to the existing service-worker.js that displays a notification when a push message arrives.
// Listen for push events
self.addEventListener('push', event => {
const data = event.data ? event.data.json() : {};
const title = data.title || 'New Update!';
const options = {
body: data.body || 'You have a new message.',
icon: '/icon-192.png',
badge: '/icon-192.png',
data: {url: data.url || '/'}
};
event.waitUntil(self.registration.showNotification(title, options));
});
// Handle notification click
self.addEventListener('notificationclick', event => {
event.notification.close();
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
});
On the client side, request permission from the user and subscribe to push messages. The following snippet should be added to index.html after the service worker registration.
// Ask user for notification permission
if ('Notification' in window && navigator.serviceWorker) {
Notification.requestPermission(status => {
if (status === 'granted') {
navigator.serviceWorker.ready.then(reg => {
reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY')
}).then(sub => {
console.log('Push subscription:', JSON.stringify(sub));
// Send subscription to your server for later use
});
});
}
});
}
// Utility to convert VAPID key
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = atob(base64);
return Uint8Array.from([...rawData].map(ch => ch.charCodeAt(0)));
}
Replace YOUR_PUBLIC_VAPID_KEY with the public key generated by your push service (e.g., using web-push npm package). When your backend sends a push payload, the service worker will intercept it and display a notification, even if the user has the page closed.
Pro tip: Always test push notifications on a real device. Desktop browsers may suppress background notifications unless the tab is open.
Performance Optimizations: Lazy Loading & Pre‑caching
While basic caching improves offline reliability, you can further boost performance by lazy‑loading non‑critical assets and pre‑caching dynamic content. The workbox library simplifies these patterns. Below is a concise example that uses Workbox to cache Google Fonts and images with a “stale‑while‑revalidate” strategy.
// Import Workbox from CDN
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.5.4/workbox-sw.js');
if (workbox) {
// Cache Google Fonts with a long expiration
workbox.routing.registerRoute(
({url}) => url.origin === 'https://fonts.googleapis.com' ||
url.origin === 'https://fonts.gstatic.com',
new workbox.strategies.CacheFirst({
cacheName: 'google-fonts',
plugins: [
new workbox.expiration.ExpirationPlugin({maxEntries: 30, maxAgeSeconds: 60 * 60 * 24 * 365})
]
})
);
// Cache images with stale‑while‑revalidate
workbox.routing.registerRoute(
({request}) => request.destination === 'image',
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'image-cache',
plugins: [
new workbox.expiration.ExpirationPlugin({maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60})
]
})
);
} else {
console.error('Workbox could not be loaded.');
}
By delegating common caching patterns to Workbox, you reduce boilerplate and gain fine‑grained control over cache lifetimes. This approach is especially valuable for large‑scale PWAs that serve thousands of assets.
Testing & Debugging PWAs
Chrome DevTools offers a dedicated “Application” panel where you can inspect the manifest, service worker status, and cache storage. Use the “Offline” toggle to simulate a disconnected environment and verify that your app still renders. Lighthouse, built into DevTools, provides a PWA audit that scores your app on installability, performance, and best practices.
When debugging service workers, remember that they run in a separate thread. The “Console” tab in the Application panel shows logs from the service worker context, making it easier to track registration errors or cache miss events.
Pro tip: During development, setself.skipWaiting()inside theinstallevent to force the new service worker to activate immediately, avoiding the “waiting” phase that can cause stale caches.
Deploying a PWA to Production
Deploying a PWA is no different than deploying any static site, but you must ensure HTTPS and correct headers. If you’re using Netlify, Vercel, or Firebase Hosting, HTTPS is provided out of the box. Add a Cache-Control header for your service worker file (e.g., no-cache) to guarantee browsers always fetch the latest version.
For larger applications, consider using a CDN to serve cached assets globally. This reduces latency and improves the “first‑contentful‑paint” metric, which Lighthouse heavily weighs in its performance score.
Real‑World PWA Checklist
- Serve over HTTPS (mandatory for service workers).
- Include a valid
manifest.jsonwith icons of at least 192 px and 512 px. - Register a service worker that caches the shell of the app.
- Implement fallback UI for offline scenarios (e.g., an “offline.html” page).
- Test installability on both Android Chrome and iOS Safari (iOS requires a
apple-touch-iconandapple-mobile-web-app-capablemeta tag). - Run Lighthouse PWA audit and address any red flags.
Beyond the Basics: Integrating with Native Features
PWAs can access many native capabilities via modern web APIs. The Media Session API lets you control audio playback from the lock screen. The File System Access API provides read/write access to user files, useful for offline editors. On Android, Trusted Web Activity (TWA) lets you publish a PWA in the Google Play Store, giving you the best of both worlds.
While not all browsers support every API, progressive enhancement ensures that your core functionality remains accessible even on older devices.
Pro tip: Use feature detection (e.g., if ('mediaSession' in navigator)) before invoking advanced APIs to avoid runtime errors.
Conclusion
Progressive Web Apps combine the reach of the web with the reliability and engagement of native applications. By mastering the manifest, service worker, and HTTPS requirements, you can deliver fast, offline‑ready experiences that work across devices. Whether you’re building a news site, an e‑commerce storefront, or an internal tool, the PWA model scales gracefully and offers tangible business benefits. Start small, iterate with tools like Workbox, and let the browser handle the heavy lifting—your users will thank you with higher retention, lower bounce rates, and more frequent interactions.