Back to blog
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.