InspectionJanuary 1, 2024Β· 16 min read
Progressive Web App (PWA) Detection: Analyze Offline Capabilities and App Features
A practical Fusebox guide to progressive web app (pwa) detection.
Progressive Web App (PWA) Detection: Analyze Offline Capabilities and App Features
Published: January 2024
Reading time: 9 minutes
Progressive Web Apps blur the line between websites and native apps. They work offline, install on devices, send push notifications, and access device features. Here's how to detect PWA capabilities, analyze implementation quality, and understand the security implications.
Why PWA Analysis Matters
The PWA Revolution
- 3x higher conversion rates than traditional web
- 50% more user engagement with push notifications
- 90% smaller than native apps on average
- Works offline - critical for reliability
- Installable - lives on home screen
Real PWA Success Stories
Twitter Lite: 65% increase in pages per session
Pinterest: 60% increase in core engagements
Starbucks: 2x daily active users
Uber: 50KB core experience works on 2G
Spotify: Replaced native desktop app with PWA
Quick PWA Detection
1. PWA Feature Scanner
// Detect PWA features and capabilities
(function detectPWAFeatures() {
console.log('π± Progressive Web App Detection\n');
const pwaFeatures = {
manifest: false,
serviceWorker: false,
https: false,
installable: false,
offline: false,
pushNotifications: false,
addToHomeScreen: false,
fullscreen: false
};
const pwaScore = { current: 0, total: 100 };
// 1. HTTPS Check (Required for PWA)
console.log('π HTTPS Check:');
pwaFeatures.https = location.protocol === 'https:';
if (pwaFeatures.https) {
console.log(' β
Site served over HTTPS');
pwaScore.current += 10;
} else {
console.log(' β Not HTTPS - PWA requires secure connection');
}
// 2. Web App Manifest
console.log('\nπ Web App Manifest:');
const manifestLink = document.querySelector('link[rel="manifest"]');
pwaFeatures.manifest = !!manifestLink;
if (manifestLink) {
console.log(' β
Manifest found:', manifestLink.href);
pwaScore.current += 15;
// Fetch and analyze manifest
fetch(manifestLink.href)
.then(response => response.json())
.then(manifest => analyzeManifest(manifest))
.catch(err => console.log(' β Error loading manifest:', err));
} else {
console.log(' β No manifest found');
console.log(' Add: <link rel="manifest" href="/manifest.json">');
}
// 3. Service Worker
console.log('\nβοΈ Service Worker:');
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistration()
.then(registration => {
pwaFeatures.serviceWorker = !!registration;
if (registration) {
console.log(' β
Service Worker registered');
console.log(` Scope: ${registration.scope}`);
console.log(` State: ${registration.active?.state || 'installing'}`);
pwaScore.current += 25;
// Check update
registration.update().then(() => {
console.log(' π Checked for updates');
});
} else {
console.log(' β No Service Worker registered');
}
});
} else {
console.log(' β Service Worker not supported');
}
// 4. Offline Capability
console.log('\nπ΄ Offline Capability:');
if (navigator.onLine) {
console.log(' Currently online - testing offline detection...');
// Check if SW handles fetch
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
pwaFeatures.offline = true;
console.log(' β
Service Worker can handle offline');
pwaScore.current += 20;
} else {
console.log(' β No offline capability detected');
}
} else {
console.log(' π΅ Currently offline!');
pwaFeatures.offline = true;
}
// 5. Installation Capability
console.log('\nπ² Installation:');
// Check if already installed
if (window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone === true) {
console.log(' β
App is running in standalone mode (installed)');
pwaFeatures.installable = true;
pwaScore.current += 10;
} else {
console.log(' Running in browser mode');
// Listen for install prompt
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
pwaFeatures.installable = true;
console.log(' β
App is installable (install prompt available)');
pwaScore.current += 10;
});
}
// 6. Push Notifications
console.log('\nπ Push Notifications:');
if ('Notification' in window && 'serviceWorker' in navigator) {
console.log(` Permission: ${Notification.permission}`);
if (Notification.permission === 'granted') {
pwaFeatures.pushNotifications = true;
console.log(' β
Push notifications enabled');
pwaScore.current += 10;
} else if (Notification.permission === 'default') {
console.log(' π Push notifications available (not requested)');
} else {
console.log(' β Push notifications denied');
}
// Check for push subscription
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.ready.then(registration => {
registration.pushManager.getSubscription().then(subscription => {
if (subscription) {
console.log(' β
Active push subscription found');
}
});
});
}
} else {
console.log(' β Push notifications not supported');
}
// 7. App-like Features
console.log('\nπ± App-like Features:');
// Fullscreen capability
if (document.fullscreenEnabled || document.webkitFullscreenEnabled) {
console.log(' β
Fullscreen API supported');
pwaFeatures.fullscreen = true;
}
// Touch icons
const appleTouchIcon = document.querySelector('link[rel="apple-touch-icon"]');
if (appleTouchIcon) {
console.log(' β
iOS touch icon found');
}
// Theme color
const themeColor = document.querySelector('meta[name="theme-color"]');
if (themeColor) {
console.log(` β
Theme color: ${themeColor.content}`);
pwaScore.current += 5;
}
// Viewport meta
const viewport = document.querySelector('meta[name="viewport"]');
if (viewport?.content.includes('width=device-width')) {
console.log(' β
Mobile-optimized viewport');
pwaScore.current += 5;
}
// Calculate final score
setTimeout(() => {
console.log('\nπ PWA Score:');
console.log(` ${pwaScore.current}/100`);
if (pwaScore.current >= 80) {
console.log(' π Excellent PWA implementation!');
} else if (pwaScore.current >= 60) {
console.log(' π Good PWA features, room for improvement');
} else if (pwaScore.current >= 40) {
console.log(' β οΈ Basic PWA features, missing key capabilities');
} else {
console.log(' β Not a PWA - missing critical features');
}
// Recommendations
console.log('\nπ‘ Recommendations:');
if (!pwaFeatures.manifest) {
console.log(' 1. Add a Web App Manifest');
}
if (!pwaFeatures.serviceWorker) {
console.log(' 2. Implement a Service Worker');
}
if (!pwaFeatures.https) {
console.log(' 3. Serve site over HTTPS');
}
if (!pwaFeatures.offline) {
console.log(' 4. Add offline functionality');
}
}, 2000);
return pwaFeatures;
})();
// Analyze manifest.json content
function analyzeManifest(manifest) {
console.log('\nπ± Manifest Analysis:');
const requiredFields = ['name', 'short_name', 'icons', 'start_url', 'display'];
const missingFields = requiredFields.filter(field => !manifest[field]);
if (missingFields.length === 0) {
console.log(' β
All required fields present');
} else {
console.log(` β Missing fields: ${missingFields.join(', ')}`);
}
// Analyze specific fields
if (manifest.name) {
console.log(` Name: "${manifest.name}"`);
}
if (manifest.display) {
console.log(` Display: ${manifest.display}`);
if (manifest.display === 'standalone' || manifest.display === 'fullscreen') {
console.log(' β
App-like display mode');
}
}
if (manifest.icons) {
const sizes = manifest.icons.map(icon => icon.sizes).filter(Boolean);
console.log(` Icons: ${manifest.icons.length} icons`);
console.log(` Sizes: ${sizes.join(', ')}`);
const has512 = manifest.icons.some(icon => icon.sizes?.includes('512'));
if (!has512) {
console.log(' β οΈ Missing 512x512 icon for splash screen');
}
}
if (manifest.background_color && manifest.theme_color) {
console.log(` Theme: ${manifest.theme_color} / ${manifest.background_color}`);
}
// Advanced features
if (manifest.shortcuts) {
console.log(` β
App shortcuts: ${manifest.shortcuts.length}`);
}
if (manifest.share_target) {
console.log(' β
Web Share Target API');
}
if (manifest.protocol_handlers) {
console.log(' β
Protocol handlers registered');
}
}
2. Service Worker Analyzer
// Deep analysis of Service Worker implementation
async function analyzeServiceWorker() {
console.log('\nπ§ Service Worker Deep Analysis\n');
if (!('serviceWorker' in navigator)) {
console.log('β Service Worker not supported');
return;
}
const registration = await navigator.serviceWorker.getRegistration();
if (!registration) {
console.log('β No Service Worker registered');
return;
}
// Basic info
console.log('π Registration Details:');
console.log(` Scope: ${registration.scope}`);
console.log(` Update state: ${registration.updateViaCache || 'imports'}`);
const sw = registration.active || registration.waiting || registration.installing;
if (sw) {
console.log(` Script URL: ${sw.scriptURL}`);
console.log(` State: ${sw.state}`);
}
// Cache analysis
if ('caches' in window) {
console.log('\nπΎ Cache Storage Analysis:');
try {
const cacheNames = await caches.keys();
console.log(` Cache stores: ${cacheNames.length}`);
let totalCached = 0;
let cacheSize = 0;
for (const cacheName of cacheNames) {
const cache = await caches.open(cacheName);
const requests = await cache.keys();
totalCached += requests.length;
console.log(`\n π ${cacheName}:`);
console.log(` Entries: ${requests.length}`);
// Categorize cached resources
const categories = {
html: 0,
css: 0,
js: 0,
images: 0,
fonts: 0,
api: 0,
other: 0
};
requests.forEach(request => {
const url = request.url;
if (url.match(/\.html?$/i) || url.endsWith('/')) categories.html++;
else if (url.match(/\.css$/i)) categories.css++;
else if (url.match(/\.js$/i)) categories.js++;
else if (url.match(/\.(jpg|jpeg|png|gif|webp|svg)$/i)) categories.images++;
else if (url.match(/\.(woff2?|ttf|otf)$/i)) categories.fonts++;
else if (url.includes('/api/')) categories.api++;
else categories.other++;
});
Object.entries(categories).forEach(([type, count]) => {
if (count > 0) {
console.log(` ${type}: ${count}`);
}
});
}
console.log(`\n Total cached resources: ${totalCached}`);
// Storage estimate
if ('storage' in navigator && 'estimate' in navigator.storage) {
const estimate = await navigator.storage.estimate();
const usedMB = (estimate.usage / 1024 / 1024).toFixed(2);
const quotaMB = (estimate.quota / 1024 / 1024).toFixed(2);
console.log(`\n Storage usage: ${usedMB}MB / ${quotaMB}MB`);
console.log(` Usage: ${((estimate.usage / estimate.quota) * 100).toFixed(1)}%`);
}
} catch (error) {
console.log(' β Error accessing cache storage:', error);
}
}
// Check caching strategies
console.log('\nπ¦ Caching Strategy Detection:');
// This is a heuristic - actual detection would require analyzing SW code
const strategies = {
'Cache First': 'Serves from cache, falls back to network',
'Network First': 'Tries network, falls back to cache',
'Stale While Revalidate': 'Serves cache, updates in background',
'Network Only': 'Always uses network',
'Cache Only': 'Only serves cached content'
};
console.log(' Common strategies:');
Object.entries(strategies).forEach(([strategy, description]) => {
console.log(` β’ ${strategy}: ${description}`);
});
// Background sync
if ('sync' in registration) {
console.log('\nπ Background Sync:');
try {
const tags = await registration.sync.getTags();
if (tags.length > 0) {
console.log(` β
Active sync tags: ${tags.join(', ')}`);
} else {
console.log(' No active background sync');
}
} catch (e) {
console.log(' Background sync available but not active');
}
}
// Push capability
if ('pushManager' in registration) {
console.log('\nπ Push Notification Capability:');
try {
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
console.log(' β
Active push subscription');
console.log(` Endpoint: ${subscription.endpoint.substring(0, 50)}...`);
} else {
console.log(' Push capable but not subscribed');
}
} catch (e) {
console.log(' Error checking push subscription');
}
}
// Update mechanism
console.log('\nπ Update Mechanism:');
console.log(' Checking for updates...');
try {
await registration.update();
console.log(' β
Update check complete');
// Listen for updates
registration.addEventListener('updatefound', () => {
console.log(' π New Service Worker available!');
});
} catch (e) {
console.log(' β Update check failed:', e.message);
}
}
analyzeServiceWorker();
3. PWA Performance Analyzer
// Analyze PWA performance metrics
function analyzePWAPerformance() {
console.log('\nβ‘ PWA Performance Analysis\n');
const metrics = {
loadTime: 0,
cacheHitRate: 0,
offlineCapability: false,
installability: {
score: 0,
issues: []
}
};
// 1. Page Load Performance
console.log('π Load Performance:');
if (performance.timing) {
const loadTime = performance.timing.loadEventEnd - performance.timing.navigationStart;
metrics.loadTime = loadTime;
console.log(` Page load time: ${loadTime}ms`);
if (loadTime < 3000) {
console.log(' β
Fast load time');
} else if (loadTime < 5000) {
console.log(' π‘ Moderate load time');
} else {
console.log(' β Slow load time - optimize for PWA');
}
}
// 2. Resource Caching Efficiency
console.log('\nπΎ Cache Efficiency:');
const resources = performance.getEntriesByType('resource');
const cachedResources = resources.filter(r =>
r.transferSize === 0 && r.decodedBodySize > 0
);
metrics.cacheHitRate = resources.length > 0 ?
(cachedResources.length / resources.length * 100).toFixed(1) : 0;
console.log(` Cache hit rate: ${metrics.cacheHitRate}%`);
console.log(` Cached resources: ${cachedResources.length}/${resources.length}`);
if (metrics.cacheHitRate > 80) {
console.log(' β
Excellent cache usage');
} else if (metrics.cacheHitRate > 50) {
console.log(' π‘ Good cache usage, room for improvement');
} else {
console.log(' β Poor cache usage - implement better caching');
}
// 3. Critical Resources
console.log('\nπ― Critical Resource Loading:');
// Check for render-blocking resources
const renderBlocking = resources.filter(r =>
r.renderBlockingStatus === 'blocking'
);
console.log(` Render-blocking resources: ${renderBlocking.length}`);
if (renderBlocking.length === 0) {
console.log(' β
No render-blocking resources');
} else {
console.log(' β οΈ Render-blocking resources found:');
renderBlocking.slice(0, 3).forEach(r => {
console.log(` - ${r.name.split('/').pop()}`);
});
}
// 4. App Shell Pattern
console.log('\nπ App Shell Detection:');
// Check if main HTML is cached (indicates app shell)
const navigationEntry = performance.getEntriesByType('navigation')[0];
const htmlCached = navigationEntry && navigationEntry.transferSize === 0;
if (htmlCached) {
console.log(' β
App shell pattern detected (HTML cached)');
metrics.offlineCapability = true;
} else {
console.log(' β No app shell pattern detected');
}
// 5. Network Resilience
console.log('\nπ Network Resilience:');
// Simulate offline check
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
console.log(' β
Service Worker active - can handle offline');
// Test fetch interception
fetch('/test-offline-capability')
.then(() => console.log(' β
Fetch handled by Service Worker'))
.catch(() => console.log(' βΉοΈ Network request (SW may not cache this)'));
} else {
console.log(' β No offline capability');
}
// 6. Installation Requirements
console.log('\nπ² Installation Requirements:');
const installChecks = {
https: location.protocol === 'https:',
serviceWorker: 'serviceWorker' in navigator && !!navigator.serviceWorker.controller,
manifest: !!document.querySelector('link[rel="manifest"]'),
icons: false,
startUrl: false,
display: false
};
// Check manifest requirements
const manifestLink = document.querySelector('link[rel="manifest"]');
if (manifestLink) {
fetch(manifestLink.href)
.then(r => r.json())
.then(manifest => {
installChecks.icons = manifest.icons?.some(i =>
i.sizes?.includes('192') || i.sizes?.includes('512')
);
installChecks.startUrl = !!manifest.start_url;
installChecks.display = ['standalone', 'fullscreen', 'minimal-ui']
.includes(manifest.display);
// Calculate install score
const passed = Object.values(installChecks).filter(v => v).length;
metrics.installability.score = (passed / Object.keys(installChecks).length * 100).toFixed(0);
console.log(` Installation score: ${metrics.installability.score}%`);
Object.entries(installChecks).forEach(([check, passed]) => {
console.log(` ${passed ? 'β
' : 'β'} ${check}`);
if (!passed) {
metrics.installability.issues.push(check);
}
});
});
}
// Performance recommendations
setTimeout(() => {
console.log('\nπ‘ Performance Recommendations:');
if (metrics.loadTime > 3000) {
console.log(' 1. Optimize initial load time');
}
if (metrics.cacheHitRate < 80) {
console.log(' 2. Implement aggressive caching strategy');
}
if (!metrics.offlineCapability) {
console.log(' 3. Implement offline-first architecture');
}
if (renderBlocking.length > 0) {
console.log(' 4. Eliminate render-blocking resources');
}
if (metrics.installability.score < 100) {
console.log(' 5. Fix installation requirements');
}
}, 1000);
return metrics;
}
analyzePWAPerformance();
Security Analysis
PWA Security Scanner
// Analyze PWA security implications
function analyzePWASecurity() {
console.log('\nπ PWA Security Analysis\n');
const security = {
score: 100,
issues: [],
recommendations: []
};
// 1. HTTPS Enforcement
console.log('π HTTPS Security:');
if (location.protocol !== 'https:') {
console.log(' β Not served over HTTPS!');
security.issues.push('No HTTPS - PWA features disabled');
security.score -= 50;
} else {
console.log(' β
Served over HTTPS');
// Check for mixed content
const mixedContent = document.querySelectorAll('[src^="http:"], [href^="http:"]');
if (mixedContent.length > 0) {
console.log(` β οΈ Mixed content detected: ${mixedContent.length} resources`);
security.issues.push('Mixed content weakens security');
security.score -= 10;
}
}
// 2. Service Worker Security
console.log('\nβοΈ Service Worker Security:');
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistration().then(reg => {
if (reg) {
// Check scope
const scope = new URL(reg.scope);
if (scope.pathname === '/') {
console.log(' β οΈ Service Worker has root scope - controls entire site');
security.recommendations.push('Limit Service Worker scope if possible');
} else {
console.log(` β
Limited scope: ${scope.pathname}`);
}
// Check update mechanism
if (reg.updateViaCache === 'all') {
console.log(' β οΈ SW updates can be cached - security risk');
security.issues.push('Service Worker updates may be delayed');
security.score -= 5;
}
}
});
}
// 3. Content Security Policy
console.log('\nπ‘οΈ Content Security Policy:');
const cspMeta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
// Check CSP compatibility with SW
if (cspMeta) {
const csp = cspMeta.content;
console.log(' β
CSP meta tag found');
if (csp.includes("'unsafe-inline'") || csp.includes("'unsafe-eval'")) {
console.log(' β οΈ Unsafe CSP directives for PWA');
security.issues.push('CSP allows unsafe scripts');
security.score -= 10;
}
if (!csp.includes('upgrade-insecure-requests')) {
console.log(' βΉοΈ Consider adding upgrade-insecure-requests');
}
} else {
console.log(' β No CSP found');
security.recommendations.push('Implement Content Security Policy');
security.score -= 15;
}
// 4. Permissions
console.log('\nπ Permissions & APIs:');
const dangerousAPIs = [
{ api: 'geolocation', risk: 'medium', check: () => 'geolocation' in navigator },
{ api: 'camera', risk: 'high', check: () => 'mediaDevices' in navigator },
{ api: 'microphone', risk: 'high', check: () => 'mediaDevices' in navigator },
{ api: 'notifications', risk: 'low', check: () => 'Notification' in window },
{ api: 'bluetooth', risk: 'high', check: () => 'bluetooth' in navigator },
{ api: 'usb', risk: 'high', check: () => 'usb' in navigator }
];
const usedAPIs = [];
dangerousAPIs.forEach(({ api, risk, check }) => {
if (check()) {
console.log(` ${risk === 'high' ? 'π΄' : risk === 'medium' ? 'π‘' : 'π’'} ${api} API available`);
// Check if permission was granted
if (api === 'notifications' && Notification.permission === 'granted') {
usedAPIs.push(api);
}
}
});
if (usedAPIs.length > 0) {
console.log(`\n β οΈ Active permissions: ${usedAPIs.join(', ')}`);
security.recommendations.push('Audit permission usage');
}
// 5. Data Storage Security
console.log('\nπΎ Data Storage Security:');
// Check for sensitive data in storage
const storageAPIs = ['localStorage', 'sessionStorage'];
let sensitiveDataFound = false;
storageAPIs.forEach(storage => {
if (window[storage].length > 0) {
console.log(` Checking ${storage}...`);
for (let i = 0; i < window[storage].length; i++) {
const key = window[storage].key(i);
// Check for sensitive patterns
if (/token|password|key|secret|auth/i.test(key)) {
console.log(` β οΈ Potentially sensitive data: ${key}`);
sensitiveDataFound = true;
}
}
}
});
if (sensitiveDataFound) {
security.issues.push('Sensitive data in localStorage');
security.score -= 10;
security.recommendations.push('Use secure storage mechanisms');
}
// 6. Network Security
console.log('\nπ Network Security:');
// Check for certificate pinning (would need actual implementation)
console.log(' βΉοΈ Certificate pinning: Not detectable from client');
// Check WebSocket usage
const wsScripts = Array.from(document.scripts)
.filter(s => s.textContent.includes('WebSocket'));
if (wsScripts.length > 0) {
console.log(' β οΈ WebSocket usage detected - ensure WSS protocol');
security.recommendations.push('Verify WebSocket connections use WSS');
}
// Final score
security.score = Math.max(0, security.score);
console.log('\nπ Security Score:', security.score + '/100');
if (security.issues.length > 0) {
console.log('\nβ Security Issues:');
security.issues.forEach(issue => console.log(` β’ ${issue}`));
}
if (security.recommendations.length > 0) {
console.log('\nπ‘ Security Recommendations:');
security.recommendations.forEach(rec => console.log(` β’ ${rec}`));
}
return security;
}
analyzePWASecurity();
Complete PWA Audit
Comprehensive PWA Assessment
// Run complete PWA audit
async function completePWAAudit() {
console.log('π± COMPLETE PROGRESSIVE WEB APP AUDIT');
console.log('β'.repeat(50));
console.log(`URL: ${window.location.href}`);
console.log(`Date: ${new Date().toLocaleDateString()}\n`);
const audit = {
score: 0,
features: {
core: { score: 0, max: 40 },
enhanced: { score: 0, max: 30 },
optimal: { score: 0, max: 30 }
},
passed: [],
failed: [],
warnings: []
};
// Phase 1: Core Requirements
console.log('π PHASE 1: CORE PWA REQUIREMENTS');
console.log('β'.repeat(40));
// HTTPS
const isHTTPS = location.protocol === 'https:';
if (isHTTPS) {
console.log('β
HTTPS enabled');
audit.features.core.score += 10;
audit.passed.push('HTTPS enabled');
} else {
console.log('β Not served over HTTPS');
audit.failed.push('HTTPS required for PWA');
}
// Service Worker
const hasServiceWorker = 'serviceWorker' in navigator;
let swRegistered = false;
if (hasServiceWorker) {
const registration = await navigator.serviceWorker.getRegistration();
swRegistered = !!registration;
if (swRegistered) {
console.log('β
Service Worker registered');
audit.features.core.score += 15;
audit.passed.push('Service Worker active');
} else {
console.log('β No Service Worker registered');
audit.failed.push('Service Worker required');
}
}
// Web App Manifest
const manifestLink = document.querySelector('link[rel="manifest"]');
if (manifestLink) {
console.log('β
Web App Manifest present');
audit.features.core.score += 10;
audit.passed.push('Web App Manifest found');
// Validate manifest
try {
const response = await fetch(manifestLink.href);
const manifest = await response.json();
if (!manifest.name || !manifest.icons || !manifest.start_url) {
audit.warnings.push('Manifest missing required fields');
audit.features.core.score -= 5;
}
} catch (e) {
audit.warnings.push('Could not validate manifest');
}
} else {
console.log('β No Web App Manifest');
audit.failed.push('Web App Manifest required');
}
// Responsive design
const viewport = document.querySelector('meta[name="viewport"]');
if (viewport?.content.includes('width=device-width')) {
console.log('β
Responsive viewport configured');
audit.features.core.score += 5;
audit.passed.push('Mobile-friendly viewport');
} else {
console.log('β No responsive viewport');
audit.failed.push('Responsive design required');
}
// Phase 2: Enhanced Features
console.log('\nπ PHASE 2: ENHANCED FEATURES');
console.log('β'.repeat(40));
// Offline capability
if (swRegistered && 'caches' in window) {
const cacheNames = await caches.keys();
if (cacheNames.length > 0) {
console.log('β
Offline caching implemented');
audit.features.enhanced.score += 10;
audit.passed.push('Offline capability');
} else {
console.log('β οΈ Service Worker present but no caches');
audit.warnings.push('Implement offline caching');
}
}
// Push notifications
if ('Notification' in window && swRegistered) {
console.log(`β
Push capability (${Notification.permission})`);
audit.features.enhanced.score += 5;
if (Notification.permission === 'granted') {
audit.features.enhanced.score += 5;
audit.passed.push('Push notifications active');
}
}
// Add to Home Screen
let a2hsReady = false;
if (manifestLink && isHTTPS && swRegistered) {
a2hsReady = true;
console.log('β
Installable (A2HS ready)');
audit.features.enhanced.score += 10;
audit.passed.push('App installable');
} else {
console.log('β Not installable');
audit.warnings.push('Fix core requirements for installability');
}
// Phase 3: Optimal Implementation
console.log('\nβ PHASE 3: OPTIMAL IMPLEMENTATION');
console.log('β'.repeat(40));
// Performance
const navigation = performance.getEntriesByType('navigation')[0];
const loadTime = navigation.loadEventEnd - navigation.fetchStart;
if (loadTime < 3000) {
console.log('β
Fast load time:', loadTime + 'ms');
audit.features.optimal.score += 10;
audit.passed.push('Fast page load');
} else {
console.log('β οΈ Slow load time:', loadTime + 'ms');
audit.warnings.push('Optimize load performance');
}
// HTTPS everywhere
const hasHTTPResources = document.querySelectorAll('[src^="http:"], [href^="http:"]').length > 0;
if (!hasHTTPResources) {
console.log('β
No mixed content');
audit.features.optimal.score += 5;
} else {
console.log('β οΈ Mixed content detected');
audit.warnings.push('Remove HTTP resources');
}
// App shell pattern
const resources = performance.getEntriesByType('resource');
const cachedResources = resources.filter(r => r.transferSize === 0 && r.decodedBodySize > 0);
const cacheRate = resources.length > 0 ? cachedResources.length / resources.length : 0;
if (cacheRate > 0.5) {
console.log('β
Good cache usage:', (cacheRate * 100).toFixed(0) + '%');
audit.features.optimal.score += 10;
audit.passed.push('Effective caching strategy');
}
// Security headers
try {
const response = await fetch(location.href, { method: 'HEAD' });
const hasCSP = response.headers.get('content-security-policy');
if (hasCSP) {
console.log('β
Content Security Policy present');
audit.features.optimal.score += 5;
}
} catch (e) {
// Can't check headers
}
// Calculate final score
audit.score = audit.features.core.score +
audit.features.enhanced.score +
audit.features.optimal.score;
const maxScore = audit.features.core.max +
audit.features.enhanced.max +
audit.features.optimal.max;
const percentage = (audit.score / maxScore * 100).toFixed(0);
// Summary
console.log('\nπ AUDIT SUMMARY');
console.log('β'.repeat(50));
console.log(`PWA Score: ${audit.score}/${maxScore} (${percentage}%)`);
console.log(`Passed checks: ${audit.passed.length}`);
console.log(`Failed checks: ${audit.failed.length}`);
console.log(`Warnings: ${audit.warnings.length}`);
// Grade
let grade = 'F';
if (percentage >= 90) grade = 'A';
else if (percentage >= 80) grade = 'B';
else if (percentage >= 70) grade = 'C';
else if (percentage >= 60) grade = 'D';
console.log(`Grade: ${grade}`);
// Recommendations
console.log('\nπ‘ TOP RECOMMENDATIONS:');
let recNumber = 1;
if (audit.failed.includes('HTTPS required for PWA')) {
console.log(`${recNumber++}. Enable HTTPS immediately`);
}
if (audit.failed.includes('Service Worker required')) {
console.log(`${recNumber++}. Implement a Service Worker`);
}
if (audit.failed.includes('Web App Manifest required')) {
console.log(`${recNumber++}. Add a Web App Manifest`);
}
if (audit.warnings.includes('Implement offline caching')) {
console.log(`${recNumber++}. Add offline functionality`);
}
if (loadTime > 3000) {
console.log(`${recNumber++}. Improve load performance`);
}
console.log(`${recNumber++}. Test with Lighthouse PWA audit`);
console.log(`${recNumber}. Monitor real user metrics`);
return audit;
}
// Run the complete audit
completePWAAudit();
The Bottom Line
PWAs are the future of web development:
- Native app features without app stores - Install, offline, push
- Performance benefits are massive - Instant loads from cache
- User engagement increases dramatically - Push + home screen
- Security requirements are strict - HTTPS, CSP, secure storage
- Implementation quality varies wildly - Test thoroughly
Build it right. Test every feature. Monitor real usage. PWAs can match native appsβwhen done properly.
Analyze PWAs instantly: Fusebox detects PWA features, analyzes Service Workers, monitors offline capability, and validates implementation while you browse. Build better progressive web apps. $29 one-time purchase.