JavaScript Progressive Web Apps: Building Native-Like Experiences
Master Progressive Web Apps with JavaScript. Learn service workers, caching strategies, offline functionality, and native app features for modern web experiences.
Progressive Web Apps (PWAs) combine the best of web and native apps, offering app-like experiences with web technologies. They provide offline functionality, push notifications, and native app features while remaining accessible through web browsers.
Understanding Progressive Web Apps
PWAs are web applications that use modern web capabilities to deliver app-like experiences. They're reliable, fast, and engaging, working offline and providing native app features.
Core PWA Technologies
// Check PWA support
function checkPWASupport() {
const support = {
serviceWorker: 'serviceWorker' in navigator,
manifest: 'manifest' in document.createElement('link'),
notification: 'Notification' in window,
pushManager: 'PushManager' in window,
cacheAPI: 'caches' in window,
};
console.log('PWA Support:', support);
return support;
}
// PWA installation detection
class PWAInstaller {
constructor() {
this.deferredPrompt = null;
this.isInstalled = false;
this.init();
}
init() {
// Listen for beforeinstallprompt event
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault();
this.deferredPrompt = event;
this.showInstallButton();
});
// Check if already installed
window.addEventListener('appinstalled', () => {
this.isInstalled = true;
this.hideInstallButton();
console.log('PWA installed successfully');
});
// Check if launched as PWA
if (
window.navigator.standalone ||
window.matchMedia('(display-mode: standalone)').matches
) {
this.isInstalled = true;
}
}
async promptInstall() {
if (!this.deferredPrompt) {
console.log('Install prompt not available');
return false;
}
this.deferredPrompt.prompt();
const { outcome } = await this.deferredPrompt.userChoice;
if (outcome === 'accepted') {
console.log('User accepted install prompt');
} else {
console.log('User dismissed install prompt');
}
this.deferredPrompt = null;
return outcome === 'accepted';
}
showInstallButton() {
const installButton = document.getElementById('install-button');
if (installButton) {
installButton.style.display = 'block';
installButton.addEventListener('click', () => this.promptInstall());
}
}
hideInstallButton() {
const installButton = document.getElementById('install-button');
if (installButton) {
installButton.style.display = 'none';
}
}
getInstallStatus() {
return {
isInstallable: !!this.deferredPrompt,
isInstalled: this.isInstalled,
isStandalone:
window.navigator.standalone ||
window.matchMedia('(display-mode: standalone)').matches,
};
}
}
Service Workers
Basic Service Worker Setup
// main.js - Register service worker
async function registerServiceWorker() {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/',
});
console.log('Service Worker registered:', registration);
// Handle updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (
newWorker.state === 'installed' &&
navigator.serviceWorker.controller
) {
// New version available
showUpdateAvailable();
}
});
});
return registration;
} catch (error) {
console.error('Service Worker registration failed:', error);
}
}
}
// sw.js - Service Worker
const CACHE_NAME = 'pwa-cache-v1';
const STATIC_CACHE = 'static-v1';
const DYNAMIC_CACHE = 'dynamic-v1';
const STATIC_FILES = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/manifest.json',
'/icons/icon-192.png',
'/icons/icon-512.png',
];
// Install event - cache static resources
self.addEventListener('install', (event) => {
console.log('Service Worker installing...');
event.waitUntil(
caches
.open(STATIC_CACHE)
.then((cache) => {
console.log('Caching static files');
return cache.addAll(STATIC_FILES);
})
.then(() => {
console.log('Static files cached');
return self.skipWaiting(); // Activate immediately
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
console.log('Service Worker activating...');
event.waitUntil(
caches
.keys()
.then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
.then(() => {
console.log('Service Worker activated');
return self.clients.claim(); // Take control immediately
})
);
});
// Fetch event - serve cached content
self.addEventListener('fetch', (event) => {
const { request } = event;
// Skip non-GET requests
if (request.method !== 'GET') {
return;
}
event.respondWith(
caches
.match(request)
.then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
// Clone request for cache
const fetchRequest = request.clone();
return fetch(fetchRequest).then((response) => {
if (
!response ||
response.status !== 200 ||
response.type !== 'basic'
) {
return response;
}
// Clone response for cache
const responseToCache = response.clone();
caches.open(DYNAMIC_CACHE).then((cache) => {
cache.put(request, responseToCache);
});
return response;
});
})
.catch(() => {
// Return offline page for navigation requests
if (request.destination === 'document') {
return caches.match('/offline.html');
}
})
);
});
Advanced Caching Strategies
// Advanced Service Worker with multiple caching strategies
class ServiceWorkerManager {
constructor() {
this.CACHE_NAME = 'pwa-advanced-v1';
this.STATIC_CACHE = 'static-v1';
this.API_CACHE = 'api-v1';
this.IMAGE_CACHE = 'images-v1';
this.strategies = {
cacheFirst: this.cacheFirst.bind(this),
networkFirst: this.networkFirst.bind(this),
staleWhileRevalidate: this.staleWhileRevalidate.bind(this),
networkOnly: this.networkOnly.bind(this),
cacheOnly: this.cacheOnly.bind(this),
};
this.routes = [
{
pattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/,
strategy: 'cacheFirst',
cache: this.IMAGE_CACHE,
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
},
{
pattern: /\/api\//,
strategy: 'networkFirst',
cache: this.API_CACHE,
maxAge: 5 * 60 * 1000, // 5 minutes
},
{
pattern: /\.(?:js|css)$/,
strategy: 'staleWhileRevalidate',
cache: this.STATIC_CACHE,
},
{
pattern: /\//,
strategy: 'networkFirst',
cache: this.STATIC_CACHE,
},
];
}
// Cache First - serve from cache, fallback to network
async cacheFirst(request, cacheName) {
const cache = await caches.open(cacheName);
const cachedResponse = await cache.match(request);
if (cachedResponse) {
return cachedResponse;
}
try {
const networkResponse = await fetch(request);
if (networkResponse.status === 200) {
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
throw error;
}
}
// Network First - try network, fallback to cache
async networkFirst(request, cacheName) {
const cache = await caches.open(cacheName);
try {
const networkResponse = await fetch(request);
if (networkResponse.status === 200) {
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
const cachedResponse = await cache.match(request);
if (cachedResponse) {
return cachedResponse;
}
throw error;
}
}
// Stale While Revalidate - serve from cache, update in background
async staleWhileRevalidate(request, cacheName) {
const cache = await caches.open(cacheName);
const cachedResponse = await cache.match(request);
const networkPromise = fetch(request)
.then((response) => {
if (response.status === 200) {
cache.put(request, response.clone());
}
return response;
})
.catch(() => cachedResponse);
return cachedResponse || networkPromise;
}
// Network Only - always fetch from network
async networkOnly(request) {
return fetch(request);
}
// Cache Only - only serve from cache
async cacheOnly(request, cacheName) {
const cache = await caches.open(cacheName);
return cache.match(request);
}
// Route request to appropriate strategy
handleRequest(request) {
const url = new URL(request.url);
for (const route of this.routes) {
if (route.pattern.test(url.pathname)) {
const strategy = this.strategies[route.strategy];
return strategy(request, route.cache);
}
}
// Default to network first
return this.networkFirst(request, this.STATIC_CACHE);
}
// Clean expired cache entries
async cleanExpiredCache() {
const cacheNames = await caches.keys();
for (const cacheName of cacheNames) {
const cache = await caches.open(cacheName);
const requests = await cache.keys();
for (const request of requests) {
const response = await cache.match(request);
const dateHeader = response.headers.get('date');
if (dateHeader) {
const cachedTime = new Date(dateHeader).getTime();
const now = Date.now();
const maxAge = this.getMaxAgeForCache(cacheName);
if (now - cachedTime > maxAge) {
await cache.delete(request);
console.log('Deleted expired cache entry:', request.url);
}
}
}
}
}
getMaxAgeForCache(cacheName) {
const route = this.routes.find((r) => r.cache === cacheName);
return route?.maxAge || 24 * 60 * 60 * 1000; // Default 1 day
}
}
// Use in service worker
const swManager = new ServiceWorkerManager();
self.addEventListener('fetch', (event) => {
event.respondWith(swManager.handleRequest(event.request));
});
// Clean expired cache periodically
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'CLEAN_CACHE') {
swManager.cleanExpiredCache();
}
});
Offline Functionality
Offline Storage Management
class OfflineManager {
constructor() {
this.dbName = 'PWAOfflineDB';
this.dbVersion = 1;
this.db = null;
this.init();
}
async init() {
this.db = await this.openDB();
this.setupOnlineOfflineListeners();
}
openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create stores
if (!db.objectStoreNames.contains('pending_requests')) {
const pendingStore = db.createObjectStore('pending_requests', {
keyPath: 'id',
autoIncrement: true,
});
pendingStore.createIndex('url', 'url', { unique: false });
pendingStore.createIndex('timestamp', 'timestamp', { unique: false });
}
if (!db.objectStoreNames.contains('offline_data')) {
const dataStore = db.createObjectStore('offline_data', {
keyPath: 'key',
});
}
};
});
}
setupOnlineOfflineListeners() {
window.addEventListener('online', () => {
console.log('Back online');
this.processPendingRequests();
this.notifyOnlineStatus(true);
});
window.addEventListener('offline', () => {
console.log('Gone offline');
this.notifyOnlineStatus(false);
});
}
// Store failed requests for later retry
async storePendingRequest(url, method, data, headers = {}) {
const transaction = this.db.transaction(['pending_requests'], 'readwrite');
const store = transaction.objectStore('pending_requests');
const request = {
url,
method,
data,
headers,
timestamp: Date.now(),
};
await store.add(request);
console.log('Stored pending request:', url);
}
// Process all pending requests when back online
async processPendingRequests() {
if (!navigator.onLine) return;
const transaction = this.db.transaction(['pending_requests'], 'readwrite');
const store = transaction.objectStore('pending_requests');
const requests = await store.getAll();
for (const request of requests) {
try {
const response = await fetch(request.url, {
method: request.method,
headers: request.headers,
body: request.data,
});
if (response.ok) {
await store.delete(request.id);
console.log('Successfully processed pending request:', request.url);
}
} catch (error) {
console.error('Failed to process pending request:', error);
}
}
}
// Store data for offline access
async storeOfflineData(key, data) {
const transaction = this.db.transaction(['offline_data'], 'readwrite');
const store = transaction.objectStore('offline_data');
await store.put({
key,
data,
timestamp: Date.now(),
});
}
// Retrieve offline data
async getOfflineData(key) {
const transaction = this.db.transaction(['offline_data'], 'readonly');
const store = transaction.objectStore('offline_data');
const result = await store.get(key);
return result?.data;
}
// Check if online
isOnline() {
return navigator.onLine;
}
// Notify app of online status changes
notifyOnlineStatus(isOnline) {
const event = new CustomEvent('onlineStatusChanged', {
detail: { isOnline },
});
window.dispatchEvent(event);
}
// Enhanced fetch with offline support
async fetchWithOfflineSupport(url, options = {}) {
try {
const response = await fetch(url, options);
if (response.ok) {
// Store successful response for offline access
if (options.method === 'GET' || !options.method) {
const data = await response.clone().json();
await this.storeOfflineData(url, data);
}
}
return response;
} catch (error) {
// If offline or network error
if (!this.isOnline() || error.name === 'TypeError') {
// For GET requests, try to serve from offline storage
if (options.method === 'GET' || !options.method) {
const offlineData = await this.getOfflineData(url);
if (offlineData) {
return new Response(JSON.stringify(offlineData), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
} else {
// For other methods, store for later retry
await this.storePendingRequest(
url,
options.method,
options.body,
options.headers
);
}
}
throw error;
}
}
}
Background Sync
// Background Sync for reliable data synchronization
class BackgroundSyncManager {
constructor() {
this.syncTags = {
DATA_SYNC: 'data-sync',
MESSAGE_SYNC: 'message-sync',
FILE_SYNC: 'file-sync',
};
}
// Register background sync
async registerSync(tag, data = {}) {
if (
'serviceWorker' in navigator &&
'sync' in window.ServiceWorkerRegistration.prototype
) {
try {
const registration = await navigator.serviceWorker.ready;
// Store sync data
await this.storeSyncData(tag, data);
// Register sync
await registration.sync.register(tag);
console.log(`Background sync registered: ${tag}`);
return true;
} catch (error) {
console.error('Background sync registration failed:', error);
// Fallback: try immediate sync
await this.performSync(tag);
return false;
}
} else {
console.warn('Background sync not supported');
return false;
}
}
// Store sync data
async storeSyncData(tag, data) {
const syncData = {
tag,
data,
timestamp: Date.now(),
retryCount: 0,
};
localStorage.setItem(`sync_${tag}`, JSON.stringify(syncData));
}
// Retrieve sync data
getSyncData(tag) {
const data = localStorage.getItem(`sync_${tag}`);
return data ? JSON.parse(data) : null;
}
// Remove sync data
removeSyncData(tag) {
localStorage.removeItem(`sync_${tag}`);
}
// Perform actual sync operation
async performSync(tag) {
const syncData = this.getSyncData(tag);
if (!syncData) {
console.warn(`No sync data found for tag: ${tag}`);
return false;
}
try {
let success = false;
switch (tag) {
case this.syncTags.DATA_SYNC:
success = await this.syncData(syncData.data);
break;
case this.syncTags.MESSAGE_SYNC:
success = await this.syncMessages(syncData.data);
break;
case this.syncTags.FILE_SYNC:
success = await this.syncFiles(syncData.data);
break;
default:
console.warn(`Unknown sync tag: ${tag}`);
}
if (success) {
this.removeSyncData(tag);
console.log(`Sync completed successfully: ${tag}`);
return true;
} else {
// Increment retry count
syncData.retryCount++;
if (syncData.retryCount < 3) {
this.storeSyncData(tag, syncData.data);
} else {
this.removeSyncData(tag);
console.error(`Sync failed after 3 retries: ${tag}`);
}
return false;
}
} catch (error) {
console.error(`Sync error for ${tag}:`, error);
return false;
}
}
// Sync data to server
async syncData(data) {
try {
const response = await fetch('/api/sync', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
return response.ok;
} catch (error) {
console.error('Data sync failed:', error);
return false;
}
}
// Sync messages
async syncMessages(messages) {
try {
const response = await fetch('/api/messages/sync', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ messages }),
});
return response.ok;
} catch (error) {
console.error('Message sync failed:', error);
return false;
}
}
// Sync files
async syncFiles(files) {
try {
for (const file of files) {
const formData = new FormData();
formData.append('file', file.blob);
formData.append('metadata', JSON.stringify(file.metadata));
const response = await fetch('/api/files/upload', {
method: 'POST',
body: formData,
});
if (!response.ok) {
return false;
}
}
return true;
} catch (error) {
console.error('File sync failed:', error);
return false;
}
}
}
// Service Worker background sync handler
self.addEventListener('sync', (event) => {
console.log('Background sync triggered:', event.tag);
event.waitUntil(
(async () => {
const syncManager = new BackgroundSyncManager();
await syncManager.performSync(event.tag);
})()
);
});
Push Notifications
class PushNotificationManager {
constructor() {
this.vapidPublicKey = 'YOUR_VAPID_PUBLIC_KEY';
this.subscription = null;
}
// Check notification support and permission
async checkSupport() {
if (!('Notification' in window)) {
throw new Error('Notifications not supported');
}
if (!('PushManager' in window)) {
throw new Error('Push messaging not supported');
}
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
throw new Error('Notification permission denied');
}
return true;
}
// Subscribe to push notifications
async subscribe() {
try {
await this.checkSupport();
const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey),
});
// Send subscription to server
await this.sendSubscriptionToServer(this.subscription);
console.log('Push subscription successful');
return this.subscription;
} catch (error) {
console.error('Push subscription failed:', error);
throw error;
}
}
// Unsubscribe from push notifications
async unsubscribe() {
if (this.subscription) {
await this.subscription.unsubscribe();
await this.removeSubscriptionFromServer(this.subscription);
this.subscription = null;
console.log('Push unsubscription successful');
}
}
// Send subscription to server
async sendSubscriptionToServer(subscription) {
try {
const response = await fetch('/api/push/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(subscription),
});
if (!response.ok) {
throw new Error('Failed to send subscription to server');
}
} catch (error) {
console.error('Error sending subscription to server:', error);
throw error;
}
}
// Remove subscription from server
async removeSubscriptionFromServer(subscription) {
try {
await fetch('/api/push/unsubscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(subscription),
});
} catch (error) {
console.error('Error removing subscription from server:', error);
}
}
// Utility function to convert VAPID key
urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
// Check current subscription status
async getSubscriptionStatus() {
try {
const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription();
return {
isSubscribed: !!this.subscription,
subscription: this.subscription,
};
} catch (error) {
console.error('Error getting subscription status:', error);
return { isSubscribed: false, subscription: null };
}
}
}
// Service Worker push event handler
self.addEventListener('push', (event) => {
let notificationData = {
title: 'New Notification',
body: 'You have a new message',
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
tag: 'default',
data: {},
};
if (event.data) {
try {
notificationData = {
...notificationData,
...event.data.json(),
};
} catch (error) {
console.error('Error parsing push data:', error);
}
}
const notificationOptions = {
body: notificationData.body,
icon: notificationData.icon,
badge: notificationData.badge,
tag: notificationData.tag,
data: notificationData.data,
actions: notificationData.actions || [],
requireInteraction: notificationData.requireInteraction || false,
silent: notificationData.silent || false,
};
event.waitUntil(
self.registration.showNotification(
notificationData.title,
notificationOptions
)
);
});
// Handle notification clicks
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const action = event.action;
const data = event.notification.data;
event.waitUntil(
(async () => {
const clients = await self.clients.matchAll({ type: 'window' });
if (action === 'open_app' || !action) {
// Open app or focus existing window
if (clients.length > 0) {
return clients[0].focus();
} else {
return self.clients.openWindow('/');
}
} else if (action === 'dismiss') {
// Just close notification
return;
} else {
// Custom action
return self.clients.openWindow(data.url || '/');
}
})()
);
});
Web App Manifest
// manifest.json
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"description": "A feature-rich Progressive Web App",
"start_url": "/",
"display": "standalone",
"orientation": "portrait-primary",
"theme_color": "#2196F3",
"background_color": "#ffffff",
"categories": ["productivity", "utilities"],
"lang": "en",
"dir": "ltr",
"scope": "/",
"icons": [
{
"src": "/icons/icon-72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/icons/icon-96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/icons/icon-128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/icons/icon-144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/icons/icon-152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"shortcuts": [
{
"name": "Create New",
"short_name": "New",
"description": "Create a new item",
"url": "/new",
"icons": [
{
"src": "/icons/shortcut-new.png",
"sizes": "192x192"
}
]
}
],
"share_target": {
"action": "/share",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url",
"files": [
{
"name": "file",
"accept": ["image/*", "text/*"]
}
]
}
},
"protocol_handlers": [
{
"protocol": "web+myapp",
"url": "/handle?protocol=%s"
}
]
}
// Manifest manager
class ManifestManager {
constructor() {
this.manifest = null;
}
async loadManifest() {
try {
const response = await fetch('/manifest.json');
this.manifest = await response.json();
return this.manifest;
} catch (error) {
console.error('Failed to load manifest:', error);
return null;
}
}
// Update manifest dynamically
updateManifest(updates) {
if (!this.manifest) return;
Object.assign(this.manifest, updates);
// Update the manifest link
const manifestLink = document.querySelector('link[rel="manifest"]');
if (manifestLink) {
const blob = new Blob([JSON.stringify(this.manifest)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
manifestLink.href = url;
}
}
// Update theme color
updateThemeColor(color) {
this.updateManifest({ theme_color: color });
// Update meta tag
let metaTheme = document.querySelector('meta[name="theme-color"]');
if (!metaTheme) {
metaTheme = document.createElement('meta');
metaTheme.name = 'theme-color';
document.head.appendChild(metaTheme);
}
metaTheme.content = color;
}
// Get manifest data
getManifest() {
return this.manifest;
}
// Check if app meets PWA criteria
checkPWACriteria() {
const criteria = {
hasManifest: !!document.querySelector('link[rel="manifest"]'),
hasServiceWorker: 'serviceWorker' in navigator,
hasIcons: this.manifest?.icons?.length > 0,
hasStartUrl: !!this.manifest?.start_url,
hasName: !!this.manifest?.name || !!this.manifest?.short_name,
hasDisplay: !!this.manifest?.display,
isServedOverHTTPS: location.protocol === 'https:' || location.hostname === 'localhost'
};
const passed = Object.values(criteria).filter(Boolean).length;
const total = Object.keys(criteria).length;
return {
criteria,
score: passed / total,
isPWA: passed === total
};
}
}
Best Practices
- App Shell Architecture
class AppShell {
constructor() {
this.shell = document.getElementById('app-shell');
this.content = document.getElementById('app-content');
this.loading = document.getElementById('loading');
}
showLoading() {
this.loading.style.display = 'flex';
this.content.style.display = 'none';
}
hideLoading() {
this.loading.style.display = 'none';
this.content.style.display = 'block';
}
updateContent(html) {
this.content.innerHTML = html;
}
// Preload critical resources
preloadCriticalResources() {
const criticalResources = [
'/styles/critical.css',
'/js/app.js',
'/icons/icon-192.png',
];
criticalResources.forEach((resource) => {
const link = document.createElement('link');
link.rel = 'preload';
link.href = resource;
link.as = this.getResourceType(resource);
document.head.appendChild(link);
});
}
getResourceType(url) {
if (url.endsWith('.css')) return 'style';
if (url.endsWith('.js')) return 'script';
if (url.match(/\.(png|jpg|jpeg|gif|webp)$/)) return 'image';
return 'fetch';
}
}
- Performance Optimization
class PWAPerformance {
constructor() {
this.metrics = {};
}
// Measure performance
measurePerformance() {
if ('performance' in window) {
const navigation = performance.getEntriesByType('navigation')[0];
this.metrics = {
dns: navigation.domainLookupEnd - navigation.domainLookupStart,
connection: navigation.connectEnd - navigation.connectStart,
request: navigation.responseStart - navigation.requestStart,
response: navigation.responseEnd - navigation.responseStart,
dom:
navigation.domContentLoadedEventEnd -
navigation.domContentLoadedEventStart,
load: navigation.loadEventEnd - navigation.loadEventStart,
total: navigation.loadEventEnd - navigation.navigationStart,
};
console.log('Performance metrics:', this.metrics);
return this.metrics;
}
}
// Monitor Core Web Vitals
monitorWebVitals() {
// Largest Contentful Paint
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.startTime);
}).observe({ entryTypes: ['largest-contentful-paint'] });
// First Input Delay
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
entries.forEach((entry) => {
console.log('FID:', entry.processingStart - entry.startTime);
});
}).observe({ entryTypes: ['first-input'] });
// Cumulative Layout Shift
let clsValue = 0;
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
entries.forEach((entry) => {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
});
console.log('CLS:', clsValue);
}).observe({ entryTypes: ['layout-shift'] });
}
}
Conclusion
Progressive Web Apps represent the future of web development, combining the reach of the web with the capabilities of native apps. By implementing service workers, offline functionality, push notifications, and following PWA best practices, you can create engaging, reliable, and fast web applications that work seamlessly across all devices and network conditions. PWAs offer users app-like experiences while providing developers with web platform advantages like easy deployment and universal accessibility.