Progressive Web Apps (PWA)
Building a PWA Project (Part 3)
Building a PWA Project (Part 3)
In this final part, we'll add advanced features, optimize performance, deploy our TaskMaster PWA, and conduct a complete audit using Lighthouse.
Advanced Caching Strategies
Enhance the service worker with multiple caching strategies:
// sw.js - Advanced caching strategies
const CACHE_NAME = 'taskmaster-v1';
const STATIC_CACHE = 'taskmaster-static-v1';
const DYNAMIC_CACHE = 'taskmaster-dynamic-v1';
const IMAGE_CACHE = 'taskmaster-images-v1';
const API_CACHE = 'taskmaster-api-v1';
// Cache size limits
const CACHE_LIMITS = {
[DYNAMIC_CACHE]: 50,
[IMAGE_CACHE]: 50,
[API_CACHE]: 20
};
// Trim cache to specified limit
async function trimCache(cacheName, maxItems) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
if (keys.length > maxItems) {
const deletePromises = keys
.slice(0, keys.length - maxItems)
.map(key => cache.delete(key));
await Promise.all(deletePromises);
}
}
// Cache-first strategy (for static assets)
async function cacheFirst(request) {
const cached = await caches.match(request);
return cached || fetch(request);
}
// Network-first strategy (for API calls)
async function networkFirst(request, cacheName) {
try {
const response = await fetch(request);
const cache = await caches.open(cacheName);
cache.put(request, response.clone());
await trimCache(cacheName, CACHE_LIMITS[cacheName]);
return response;
} catch (error) {
const cached = await caches.match(request);
return cached || Promise.reject(error);
}
}
// Stale-while-revalidate strategy
async function staleWhileRevalidate(request, cacheName) {
const cached = await caches.match(request);
const fetchPromise = fetch(request).then(response => {
const cache = caches.open(cacheName);
cache.then(c => c.put(request, response.clone()));
return response;
});
return cached || fetchPromise;
}
// Updated fetch handler
self.addEventListener('fetch', event => {
const { request } = event;
const url = new URL(request.url);
// API requests - network first
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(request, API_CACHE));
return;
}
// Images - cache first
if (request.destination === 'image') {
event.respondWith(
caches.open(IMAGE_CACHE).then(cache => {
return cache.match(request).then(cached => {
return cached || fetch(request).then(response => {
cache.put(request, response.clone());
trimCache(IMAGE_CACHE, CACHE_LIMITS[IMAGE_CACHE]);
return response;
});
});
})
);
return;
}
// Static assets - cache first
if (request.destination === 'script' ||
request.destination === 'style' ||
request.destination === 'font') {
event.respondWith(cacheFirst(request));
return;
}
// HTML pages - stale while revalidate
if (request.destination === 'document') {
event.respondWith(
staleWhileRevalidate(request, DYNAMIC_CACHE)
.catch(() => caches.match('/offline.html'))
);
return;
}
// Default - network with cache fallback
event.respondWith(
fetch(request)
.catch(() => caches.match(request))
);
});
Performance Optimization
Implement lazy loading for images and code splitting:
// Lazy loading images
document.addEventListener('DOMContentLoaded', () => {
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
observer.unobserve(img);
}
});
});
document.querySelectorAll('img.lazy').forEach(img => {
imageObserver.observe(img);
});
});
// Dynamic import for analytics (code splitting)
async function initAnalytics() {
if (navigator.onLine) {
const analytics = await import('./analytics.js');
analytics.init();
}
}
// Request idle callback for non-critical tasks
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
initAnalytics();
preloadNextPage();
});
} else {
setTimeout(() => {
initAnalytics();
preloadNextPage();
}, 1000);
}
Analytics Integration
// analytics.js - Track PWA usage
class PWAAnalytics {
constructor() {
this.events = [];
this.sessionId = this.generateSessionId();
}
generateSessionId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
track(eventName, eventData = {}) {
const event = {
name: eventName,
data: eventData,
timestamp: new Date().toISOString(),
sessionId: this.sessionId,
online: navigator.onLine
};
this.events.push(event);
// Send to server if online
if (navigator.onLine) {
this.sendEvents();
}
}
async sendEvents() {
if (this.events.length === 0) return;
try {
await fetch('/api/analytics', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
events: this.events
})
});
this.events = [];
} catch (error) {
console.error('Failed to send analytics:', error);
}
}
trackInstall() {
this.track('pwa_installed', {
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language
});
}
trackOffline() {
this.track('app_offline');
}
trackOnline() {
this.track('app_online');
}
trackTaskAction(action, taskData) {
this.track(`task_${action}`, {
priority: taskData.priority,
hasDescription: !!taskData.description,
hasDueDate: !!taskData.dueDate
});
}
}
// Initialize analytics
const analytics = new PWAAnalytics();
// Track app lifecycle events
window.addEventListener('appinstalled', () => {
analytics.trackInstall();
});
window.addEventListener('online', () => {
analytics.trackOnline();
analytics.sendEvents();
});
window.addEventListener('offline', () => {
analytics.trackOffline();
});
export { analytics };
Testing with Lighthouse
Run a Lighthouse audit to check PWA compliance:
# Install Lighthouse CLI
npm install -g lighthouse
# Run audit
lighthouse https://your-app-url.com --view
# Or use Chrome DevTools
# 1. Open Chrome DevTools (F12)
# 2. Go to "Lighthouse" tab
# 3. Select "Progressive Web App" category
# 4. Click "Generate report"
PWA Lighthouse Checklist: Your app should score 90+ in all categories. Key requirements include HTTPS, service worker, web app manifest, responsive design, fast load time, and offline functionality.
PWA Audit Checklist
✓ Progressive Web App Requirements:
✓ Served over HTTPS
✓ Registers a service worker
✓ Has a web app manifest with:
✓ name or short_name
✓ icons (including 512x512)
✓ start_url
✓ display (standalone/fullscreen/minimal-ui)
✓ theme_color
✓ Viewport meta tag present
✓ Content sized correctly for viewport
✓ Has a 200 response when offline
✓ Installable
✓ Fast load time (< 3 seconds)
✓ Accessible (ARIA labels, semantic HTML)
✓ SEO optimized (meta descriptions, titles)
✓ Performance Optimizations:
✓ Minified CSS/JS
✓ Compressed images
✓ Lazy loading implemented
✓ Critical CSS inlined
✓ Code splitting
✓ Resource hints (preload, prefetch)
✓ HTTP/2 or HTTP/3
✓ CDN for static assets
✓ Advanced Features:
✓ Push notifications
✓ Background sync
✓ Share target API
✓ Shortcuts
✓ File handling
✓ Badge API
Deployment Configuration
Configure your server for PWA hosting:
# Apache .htaccess
<IfModule mod_headers.c>
# Service Worker - no cache
<FilesMatch "sw\.js$">
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires 0
</FilesMatch>
# Manifest
<FilesMatch "manifest\.json$">
Header set Content-Type "application/manifest+json"
Header set Cache-Control "public, max-age=604800"
</FilesMatch>
# Force HTTPS
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]
</IfModule>
# Security headers
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
# Enable compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
AddOutputFilterByType DEFLATE application/javascript application/json
AddOutputFilterByType DEFLATE application/manifest+json
</IfModule>
# Nginx configuration
server {
listen 443 ssl http2;
server_name your-domain.com;
# SSL configuration
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Service Worker - no cache
location ~ /sw\.js$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires 0;
}
# Manifest
location ~ /manifest\.json$ {
add_header Content-Type "application/manifest+json";
add_header Cache-Control "public, max-age=604800";
}
# Static assets - long cache
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
gzip_min_length 1000;
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name your-domain.com;
return 301 https://$server_name$request_uri;
}
Final Optimizations
// Preload critical resources
<link rel="preload" href="/css/styles.css" as="style">
<link rel="preload" href="/js/app.js" as="script">
<link rel="preconnect" href="https://fonts.googleapis.com">
// Prefetch next page
<link rel="prefetch" href="/settings.html">
// DNS prefetch for external resources
<link rel="dns-prefetch" href="https://api.example.com">
// Critical CSS inline
<style>
/* Critical above-the-fold styles */
body { margin: 0; font-family: sans-serif; }
.app-header { background: #2196F3; color: white; padding: 1rem; }
</style>
// Load full stylesheet asynchronously
<link rel="stylesheet" href="/css/styles.css" media="print" onload="this.media='all'">
Error Tracking and Monitoring
// Global error handler
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
// Send error to analytics
if (analytics) {
analytics.track('error', {
message: event.error.message,
stack: event.error.stack,
filename: event.filename,
lineno: event.lineno,
colno: event.colno
});
}
});
// Unhandled promise rejection
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled rejection:', event.reason);
if (analytics) {
analytics.track('unhandled_rejection', {
reason: event.reason?.toString(),
promise: event.promise
});
}
});
// Service Worker error handling
navigator.serviceWorker.addEventListener('error', (event) => {
console.error('Service Worker error:', event);
if (analytics) {
analytics.track('sw_error', {
message: event.message
});
}
});
Deployment Checklist
Before deploying:
- ✓ Test on real devices (Android, iOS, desktop)
- ✓ Verify offline functionality
- ✓ Run Lighthouse audit (score 90+)
- ✓ Test install flow
- ✓ Verify push notifications work
- ✓ Check background sync
- ✓ Test on slow 3G connection
- ✓ Verify HTTPS is enforced
- ✓ Check console for errors
- ✓ Validate manifest.json
- ✓ Test service worker updates
Monitoring Production
// Track performance metrics
if ('PerformanceObserver' in window) {
// Track Largest Contentful Paint (LCP)
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
analytics.track('performance_lcp', {
value: lastEntry.renderTime || lastEntry.loadTime
});
});
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
// Track First Input Delay (FID)
const fidObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
analytics.track('performance_fid', {
value: entry.processingStart - entry.startTime
});
});
});
fidObserver.observe({ entryTypes: ['first-input'] });
// Track Cumulative Layout Shift (CLS)
let clsScore = 0;
const clsObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsScore += entry.value;
}
}
});
clsObserver.observe({ entryTypes: ['layout-shift'] });
window.addEventListener('beforeunload', () => {
analytics.track('performance_cls', { value: clsScore });
});
}
Common Pitfalls: Don't cache the service worker file itself. Always set
Cache-Control: no-cache for sw.js. Update the cache version number when deploying changes. Test service worker updates thoroughly.
Final Exercise: Deploy your TaskMaster PWA to a production server with HTTPS. Run a Lighthouse audit and achieve a score of 90+ in all categories. Test the app on at least 3 different devices (desktop, Android, iOS). Document any issues found and their solutions.
Congratulations! You've built a complete, production-ready Progressive Web App with offline support, push notifications, background sync, and advanced caching strategies. Your app is installable, fast, and provides a native-like experience across all platforms.