Building Progressive Web Apps with Service Workers
Progressive Web Apps (PWAs) bridge the gap between web and native applications, offering app-like experiences directly in the browser. At the heart of PWAs are Service Workers—powerful scripts that enable offline functionality, background sync, and push notifications.
What are Progressive Web Apps?
PWAs are web applications that use modern web capabilities to provide users with an app-like experience. They combine the best of web and mobile apps.
Key PWA Features:
- Offline functionality through Service Workers
- App-like interface with responsive design
- Push notifications for user engagement
- Installable on device home screens
- Secure (HTTPS required)
- Progressive enhancement for all browsers
Understanding Service Workers
Service Workers are scripts that run in the background, separate from your web page, enabling features that don't need a web page or user interaction.
Service Worker Capabilities:
- Intercept network requests and serve cached responses
- Background sync for offline actions
- Push notifications from servers
- Cache management for offline content
Basic Service Worker Registration:
// main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registered: ', registration);
})
.catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}
Implementing Offline Functionality
Cache Strategies
Different caching strategies serve different use cases:
1. Cache First Strategy
// sw.js
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Return cached version or fetch from network
return response || fetch(event.request);
})
);
});
2. Network First Strategy
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request)
.then(response => {
// Clone and cache the response
const responseClone = response.clone();
caches.open('v1').then(cache => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => {
// Fallback to cache if network fails
return caches.match(event.request);
})
);
});
3. Stale While Revalidate
self.addEventListener('fetch', event => {
event.respondWith(
caches.open('v1').then(cache => {
return cache.match(event.request).then(cachedResponse => {
const fetchPromise = fetch(event.request).then(networkResponse => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
// Return cached response immediately, update cache in background
return cachedResponse || fetchPromise;
});
})
);
});
Complete Service Worker Example
// sw.js
const CACHE_NAME = 'pwa-cache-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png',
'/offline.html'
];
// Install event - cache resources
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
return cache.addAll(urlsToCache);
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
// Fetch event - serve cached content when offline
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Return cached version or fetch from network
if (response) {
return response;
}
return fetch(event.request).then(response => {
// Don't cache non-successful responses
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Clone the response
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
});
})
.catch(() => {
// Show offline page for navigation requests
if (event.request.destination === 'document') {
return caches.match('/offline.html');
}
})
);
});
Web App Manifest
The Web App Manifest provides metadata about your application:
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"description": "A sample progressive web application",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
Link the manifest in your HTML:
<link rel="manifest" href="/manifest.json">
Push Notifications
Setting Up Push Notifications
// Request notification permission
function requestNotificationPermission() {
return Notification.requestPermission().then(permission => {
if (permission === 'granted') {
console.log('Notification permission granted');
return subscribeUserToPush();
}
});
}
// Subscribe to push notifications
function subscribeUserToPush() {
return navigator.serviceWorker.register('/sw.js')
.then(registration => {
const subscribeOptions = {
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicVapidKey)
};
return registration.pushManager.subscribe(subscribeOptions);
})
.then(pushSubscription => {
console.log('User is subscribed:', pushSubscription);
return sendSubscriptionToBackEnd(pushSubscription);
});
}
// Handle push events in service worker
self.addEventListener('push', event => {
const options = {
body: event.data ? event.data.text() : 'No payload',
icon: '/icons/icon-192x192.png',
badge: '/icons/badge-72x72.png',
actions: [
{
action: 'explore',
title: 'Explore',
icon: '/icons/checkmark.png'
},
{
action: 'close',
title: 'Close',
icon: '/icons/xmark.png'
}
]
};
event.waitUntil(
self.registration.showNotification('PWA Notification', options)
);
});
Background Sync
Background Sync allows you to defer actions until the user has stable connectivity:
// Register background sync
navigator.serviceWorker.ready.then(registration => {
return registration.sync.register('background-sync');
});
// Handle background sync in service worker
self.addEventListener('sync', event => {
if (event.tag === 'background-sync') {
event.waitUntil(doBackgroundSync());
}
});
function doBackgroundSync() {
return fetch('/api/sync-data', {
method: 'POST',
body: JSON.stringify(getStoredData())
});
}
PWA Best Practices
1. App Shell Architecture
Create a minimal HTML, CSS, and JavaScript that powers the user interface:
const appShellFiles = [
'/',
'/app-shell.html',
'/styles/app-shell.css',
'/scripts/app-shell.js'
];
// Cache app shell during install
self.addEventListener('install', event => {
event.waitUntil(
caches.open('app-shell-v1')
.then(cache => cache.addAll(appShellFiles))
);
});
2. Performance Optimization
- Lazy load non-critical resources
- Preload critical resources
- Optimize images for different screen sizes
- Minimize JavaScript bundles
3. Responsive Design
Ensure your PWA works across all device sizes:
/* Responsive design for PWA */
@media (max-width: 768px) {
.container {
padding: 1rem;
}
}
@media (display-mode: standalone) {
/* Styles when app is installed */
.install-prompt {
display: none;
}
}
Testing PWAs
Tools for PWA Testing:
- Lighthouse - Automated PWA auditing
- Chrome DevTools - Application panel for debugging
- PWA Builder - Microsoft's PWA testing tool
- Workbox - Google's PWA library
Lighthouse PWA Checklist:
- ✅ Served over HTTPS
- ✅ Responsive design
- ✅ Offline functionality
- ✅ Web app manifest
- ✅ Service worker registered
- ✅ Fast load times
Deployment Considerations
HTTPS Requirement
PWAs require HTTPS for security. Service Workers only work over secure connections.
App Store Distribution
PWAs can be distributed through:
- Google Play Store (via Trusted Web Activities)
- Microsoft Store (via PWA Builder)
- Direct installation from browsers
Common Pitfalls and Solutions
1. Cache Management
// Avoid: Caching everything
// Do: Selective caching based on request type
self.addEventListener('fetch', event => {
// Only cache GET requests
if (event.request.method !== 'GET') return;
// Skip caching for API calls that should always be fresh
if (event.request.url.includes('/api/live-data')) return;
// Cache static assets
if (event.request.destination === 'image' ||
event.request.destination === 'script' ||
event.request.destination === 'style') {
event.respondWith(cacheFirst(event.request));
}
});
2. Service Worker Updates
// Handle service worker updates gracefully
self.addEventListener('message', event => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
// In your main app
navigator.serviceWorker.addEventListener('controllerchange', () => {
// Reload page when new service worker takes control
window.location.reload();
});
Conclusion
Progressive Web Apps with Service Workers offer a powerful way to create app-like experiences on the web. They provide offline functionality, push notifications, and improved performance while maintaining the reach and accessibility of web applications.
Start with basic offline functionality, then gradually add features like push notifications and background sync. Remember to test thoroughly across different devices and network conditions.
Resources
- PWA Documentation - MDN
- Workbox - Google's PWA Library
- PWA Builder - Microsoft
- Service Worker Cookbook
PWAs represent the future of web applications, combining the best of web and native app experiences. Start building your PWA today and provide users with fast, reliable, and engaging experiences.
About Tridip Dutta
Creative Developer passionate about creating innovative digital experiences and exploring AI. I love sharing knowledge to help developers build better apps.
