Progressive Web Apps (PWA)

Building a PWA Project (Part 3)

20 min Lesson 28 of 30

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.