PWA Implementation - Complete Guide
Published: September 25, 2024 | Reading time: 26 minutes
PWA Implementation Overview
Progressive Web Apps provide native app-like experiences in the browser:
PWA Benefits
# PWA Benefits
- Offline functionality
- App-like experience
- Push notifications
- Installable
- Fast loading
- Responsive design
- Secure (HTTPS)
Service Worker Implementation
Service Worker Setup and Caching
Service Worker Implementation
# Service Worker Implementation
# 1. Basic Service Worker Registration
// app.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('Service Worker registered successfully:', registration);
} catch (error) {
console.log('Service Worker registration failed:', error);
}
});
}
// sw.js
const CACHE_NAME = 'my-app-cache-v1';
const urlsToCache = [
'/',
'/static/css/main.css',
'/static/js/main.js',
'/images/logo.png',
'/manifest.json'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
# 2. Advanced Caching Strategies
// sw.js - Advanced caching
const CACHE_NAME = 'my-app-cache-v2';
const STATIC_CACHE = 'static-cache-v1';
const DYNAMIC_CACHE = 'dynamic-cache-v1';
// Install event
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then((cache) => {
return cache.addAll([
'/',
'/static/css/main.css',
'/static/js/main.js',
'/images/logo.png',
'/manifest.json'
]);
})
);
self.skipWaiting();
});
// Activate event
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) {
return caches.delete(cacheName);
}
})
);
})
);
self.clients.claim();
});
// Fetch event with different strategies
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Cache first strategy for static assets
if (request.destination === 'image' ||
request.destination === 'style' ||
request.destination === 'script') {
event.respondWith(cacheFirst(request));
}
// Network first strategy for API calls
else if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(request));
}
// Stale while revalidate for HTML pages
else {
event.respondWith(staleWhileRevalidate(request));
}
});
// Cache first strategy
async function cacheFirst(request) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const cache = await caches.open(STATIC_CACHE);
cache.put(request, networkResponse.clone());
}
return networkResponse;
}
// Network first strategy
async function networkFirst(request) {
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
return new Response('Offline', { status: 503 });
}
}
// Stale while revalidate strategy
async function staleWhileRevalidate(request) {
const cache = await caches.open(DYNAMIC_CACHE);
const cachedResponse = await cache.match(request);
const fetchPromise = fetch(request).then((networkResponse) => {
if (networkResponse.ok) {
cache.put(request, networkResponse.clone());
}
return networkResponse;
});
return cachedResponse || fetchPromise;
}
# 3. Background Sync
// sw.js - Background sync
self.addEventListener('sync', (event) => {
if (event.tag === 'background-sync') {
event.waitUntil(doBackgroundSync());
}
});
async function doBackgroundSync() {
try {
const requests = await getStoredRequests();
for (const request of requests) {
try {
await fetch(request.url, request.options);
await removeStoredRequest(request.id);
} catch (error) {
console.log('Background sync failed:', error);
}
}
} catch (error) {
console.log('Background sync error:', error);
}
}
// Store requests when offline
async function storeRequest(url, options) {
const request = {
id: Date.now(),
url,
options,
timestamp: Date.now()
};
const requests = await getStoredRequests();
requests.push(request);
localStorage.setItem('offline-requests', JSON.stringify(requests));
// Register background sync
if ('serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype) {
const registration = await navigator.serviceWorker.ready;
registration.sync.register('background-sync');
}
}
async function getStoredRequests() {
const stored = localStorage.getItem('offline-requests');
return stored ? JSON.parse(stored) : [];
}
async function removeStoredRequest(id) {
const requests = await getStoredRequests();
const filtered = requests.filter(req => req.id !== id);
localStorage.setItem('offline-requests', JSON.stringify(filtered));
}
# 4. Push Notifications
// sw.js - Push notifications
self.addEventListener('push', (event) => {
const options = {
body: event.data ? event.data.text() : 'You have a new notification',
icon: '/images/icon-192x192.png',
badge: '/images/badge-72x72.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'explore',
title: 'View',
icon: '/images/checkmark.png'
},
{
action: 'close',
title: 'Close',
icon: '/images/xmark.png'
}
]
};
event.waitUntil(
self.registration.showNotification('My App', options)
);
});
// Handle notification clicks
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'explore') {
event.waitUntil(
clients.openWindow('/')
);
} else if (event.action === 'close') {
// Just close the notification
} else {
// Default action
event.waitUntil(
clients.openWindow('/')
);
}
});
# 5. Web App Manifest
// manifest.json
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"description": "A sample Progressive Web App",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"orientation": "portrait-primary",
"scope": "/",
"lang": "en",
"dir": "ltr",
"icons": [
{
"src": "/images/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/images/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/images/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/images/icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/images/icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/images/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/images/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/images/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"categories": ["productivity", "utilities"],
"screenshots": [
{
"src": "/images/screenshot1.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
},
{
"src": "/images/screenshot2.png",
"sizes": "750x1334",
"type": "image/png",
"form_factor": "narrow"
}
],
"shortcuts": [
{
"name": "New Item",
"short_name": "New",
"description": "Create a new item",
"url": "/new",
"icons": [
{
"src": "/images/shortcut-new.png",
"sizes": "96x96"
}
]
}
]
}
# 6. PWA Installation
// app.js - PWA installation
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault();
deferredPrompt = event;
// Show install button
const installButton = document.getElementById('install-button');
installButton.style.display = 'block';
installButton.addEventListener('click', installApp);
});
async function installApp() {
if (deferredPrompt) {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
console.log('User accepted the install prompt');
} else {
console.log('User dismissed the install prompt');
}
deferredPrompt = null;
// Hide install button
const installButton = document.getElementById('install-button');
installButton.style.display = 'none';
}
}
// Check if app is already installed
window.addEventListener('appinstalled', (event) => {
console.log('PWA was installed');
});
// Detect if running as PWA
function isPWA() {
return window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone === true;
}
# 7. Offline Page
// offline.html
Offline - My App
You're Offline
It looks like you're not connected to the internet. Some features may not be available.
# 8. PWA Update Management
// app.js - PWA update management
let registration;
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
registration = await navigator.serviceWorker.register('/sw.js');
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// New content is available
showUpdateNotification();
} else {
// Content is cached for the first time
console.log('Content is cached for offline use');
}
}
});
});
});
}
function showUpdateNotification() {
const updateNotification = document.createElement('div');
updateNotification.innerHTML = `
New version available!
`;
document.body.appendChild(updateNotification);
}
function updateApp() {
if (registration && registration.waiting) {
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
window.location.reload();
}
}
# 9. PWA Testing
// pwa-test.js
class PWATester {
constructor() {
this.results = {};
}
async runTests() {
console.log('Running PWA tests...');
this.results.serviceWorker = await this.testServiceWorker();
this.results.manifest = await this.testManifest();
this.results.https = await this.testHTTPS();
this.results.responsive = await this.testResponsive();
this.results.offline = await this.testOffline();
this.generateReport();
}
async testServiceWorker() {
if (!('serviceWorker' in navigator)) {
return { status: 'fail', message: 'Service Worker not supported' };
}
try {
const registration = await navigator.serviceWorker.getRegistration();
if (registration) {
return { status: 'pass', message: 'Service Worker registered' };
} else {
return { status: 'fail', message: 'Service Worker not registered' };
}
} catch (error) {
return { status: 'fail', message: error.message };
}
}
async testManifest() {
try {
const response = await fetch('/manifest.json');
if (response.ok) {
const manifest = await response.json();
const requiredFields = ['name', 'short_name', 'start_url', 'display', 'icons'];
const missingFields = requiredFields.filter(field => !manifest[field]);
if (missingFields.length === 0) {
return { status: 'pass', message: 'Manifest is valid' };
} else {
return { status: 'fail', message: `Missing fields: ${missingFields.join(', ')}` };
}
} else {
return { status: 'fail', message: 'Manifest not found' };
}
} catch (error) {
return { status: 'fail', message: error.message };
}
}
async testHTTPS() {
if (location.protocol === 'https:') {
return { status: 'pass', message: 'HTTPS enabled' };
} else {
return { status: 'fail', message: 'HTTPS required for PWA' };
}
}
async testResponsive() {
const viewport = document.querySelector('meta[name="viewport"]');
if (viewport) {
return { status: 'pass', message: 'Viewport meta tag present' };
} else {
return { status: 'fail', message: 'Viewport meta tag missing' };
}
}
async testOffline() {
try {
const response = await fetch('/offline.html');
if (response.ok) {
return { status: 'pass', message: 'Offline page available' };
} else {
return { status: 'fail', message: 'Offline page not found' };
}
} catch (error) {
return { status: 'fail', message: error.message };
}
}
generateReport() {
console.log('PWA Test Results:');
Object.entries(this.results).forEach(([test, result]) => {
const status = result.status === 'pass' ? '✓' : '✗';
console.log(`${status} ${test}: ${result.message}`);
});
}
}
// Run tests
const tester = new PWATester();
tester.runTests();
# 10. PWA Performance Monitoring
// pwa-performance.js
class PWAPerformanceMonitor {
constructor() {
this.metrics = {};
}
startMonitoring() {
this.monitorServiceWorker();
this.monitorCacheUsage();
this.monitorOfflineUsage();
}
monitorServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', (event) => {
if (event.data.type === 'PERFORMANCE_METRIC') {
this.metrics[event.data.name] = event.data.value;
}
});
}
}
monitorCacheUsage() {
if ('storage' in navigator && 'estimate' in navigator.storage) {
navigator.storage.estimate().then((estimate) => {
this.metrics.cacheUsage = {
used: estimate.usage,
quota: estimate.quota,
percentage: (estimate.usage / estimate.quota) * 100
};
});
}
}
monitorOfflineUsage() {
window.addEventListener('online', () => {
this.metrics.lastOnline = Date.now();
});
window.addEventListener('offline', () => {
this.metrics.lastOffline = Date.now();
});
}
getMetrics() {
return this.metrics;
}
}
const monitor = new PWAPerformanceMonitor();
monitor.startMonitoring();
PWA Best Practices
PWA Implementation Guidelines
PWA Components
- Service Worker
- Web App Manifest
- HTTPS
- Responsive Design
- Offline Functionality
- Push Notifications
- App Shell
PWA Features
- Installable
- Offline-first
- Fast loading
- App-like experience
- Background sync
- Push notifications
- Native integration
Summary
PWA implementation involves several key components:
- Service Worker: Offline functionality, caching, and background sync
- Web App Manifest: App metadata and installation behavior
- HTTPS: Required for service workers and secure features
- Responsive Design: Works across all devices and screen sizes
Need More Help?
Struggling with PWA implementation or need help creating a Progressive Web App? Our frontend experts can help you build engaging, offline-capable web applications.
Get PWA Help