Progressive Web Apps: Building Offline-First Applications
Progressive Web Apps (PWAs) have changed how we think about web applications. They're not just websites—they're web applications that can work offline, send push notifications, and even be installed on devices like native apps. In this article, I'll show you how to build an offline-first PWA.
What makes an app "progressive"?
A Progressive Web App is a web application that uses modern web capabilities to provide a user experience similar to native apps. The key characteristics of PWAs are:
- Responsive: Works on any device and screen size
- Offline-capable: Works without an internet connection
- Installable: Can be added to the home screen
- Safe: Served over HTTPS
- Engagement: Push notifications and background sync
Service Workers: The heart of PWAs
Service workers are the technology that makes PWAs possible. They're JavaScript files that run in the background, separate from your main application. Service workers can:
- Intercept network requests
- Cache resources
- Handle push notifications
- Perform background sync
Let's start with a basic service worker setup:
// public/sw.js
const CACHE_NAME = 'my-pwa-v1';
const urlsToCache = [
'/',
'/static/css/main.css',
'/static/js/main.js',
'/manifest.json'
];
// Install event - cache resources
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
// Fetch event - serve from cache when offline
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Return cached version or fetch from network
return response || fetch(event.request);
})
);
});Caching strategies
There are different strategies for caching resources:
- Cache First: Check cache first, then network
- Network First: Check network first, then cache
- Stale While Revalidate: Serve cached version immediately, then update in background
- Cache Only: Only serve from cache
For an offline-first approach, cache-first is usually best:
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
return response; // Return cached version
}
return fetch(event.request).then(response => {
// Don't cache non-GET requests
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => cache.put(event.request, responseToCache));
return response;
});
})
);
});The Web App Manifest
To make your PWA installable, you need a web app manifest. This JSON file tells the browser how your app should behave when installed:
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"description": "A progressive web application",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}Offline UI and user experience
When your app is offline, you need to provide appropriate feedback to users. Create an offline page or show a message:
// Check online status
function updateOnlineStatus() {
const statusElement = document.getElementById('status');
if (navigator.onLine) {
statusElement.textContent = 'Online';
statusElement.className = 'online';
} else {
statusElement.textContent = 'Offline';
statusElement.className = 'offline';
}
}
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);Handling form submissions offline
One of the powerful features of PWAs is background sync. You can queue actions when offline and sync them when the connection returns:
// Register for background sync
if ('serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype) {
navigator.serviceWorker.ready.then(registration => {
// Register sync when user submits a form
document.getElementById('myForm').addEventListener('submit', event => {
event.preventDefault();
registration.sync.register('form-submit')
.then(() => console.log('Sync registered'))
.catch(err => console.error('Sync registration failed', err));
});
});
}
// In service worker
self.addEventListener('sync', event => {
if (event.tag === 'form-submit') {
event.waitUntil(doFormSubmit());
}
});Testing your PWA
Use these tools to test your PWA:
- Lighthouse: Chrome DevTools audit for PWA features
- Application panel: Chrome DevTools for inspecting service workers and cache
- Web App Manifest validator: Check your manifest file
Deployment considerations
When deploying your PWA:
- Serve over HTTPS
- Pre-cache critical resources
- Update service worker version numbers for cache invalidation
- Test on multiple devices and browsers
PWAs are becoming increasingly important as users expect web apps to work like native apps. With service workers and modern web APIs, you can create applications that provide excellent offline experiences and compete with native applications.
Related articles