name: pwa-review description: Comprehensive 192-point PWA audit beyond Lighthouse - analyzes manifest, service worker, offline capabilities, real-time resilience, security, iOS compatibility, and advanced PWA features user_invocable: true args:
- name: url description: The PWA URL to analyze (e.g., https://example.com) required: true
PWA Review Skill
A comprehensive Progressive Web App audit that goes beyond standard Lighthouse testing. This skill analyzes PWAs across 11 categories with a 192-point scoring system, including real-time connection resilience, advanced features, and iOS-specific compatibility checks that typical audits miss.
Scoring Overview
| Category | Points | Focus |
|---|---|---|
| Manifest Compliance | 20 | Essential manifest fields |
| Advanced Manifest | 15 | Enhanced manifest features + iOS splash |
| Service Worker & Caching | 33 | SW implementation quality + caching strategies |
| Offline Capability | 24 | Offline functionality + storage + sync + real-time resilience |
| Installability | 13 | Install requirements |
| Security | 16 | Security measures |
| Performance Signals | 17 | Performance optimization + network detection |
| UX & Accessibility | 29 | User experience + iOS safe areas + mobile dropdowns + themes + state persistence |
| SEO & Discoverability | 7 | Search optimization |
| PWA Advanced | 17 | Cutting-edge PWA features |
| iOS Compatibility | 1 | iOS-specific meta tags (bonus) |
Grading Scale: A+ (173+, 90%+), A (154-172, 80-89%), B (135-153, 70-79%), C (116-134, 60-69%), D (77-115, 40-59%), F (<77, <40%)
Execution Workflow
When the user invokes /pwa-review <url>, follow these steps precisely:
Step 1: Fetch Target HTML
Use WebFetch to retrieve the target URL's HTML content.
WebFetch: {url}
Prompt: "Return the complete HTML source code. I need to analyze the head section for PWA-related tags including manifest link, meta tags, and inline scripts."
Step 2: Extract PWA Resources
From the HTML, identify:
Manifest URL:
- Look for
<link rel="manifest" href="..."> - Convert relative URLs to absolute using the base URL
- If not found, note as CRITICAL issue
Service Worker Registration:
- Search for
navigator.serviceWorker.register('...')ornavigator.serviceWorker.register("...") - Extract the SW file path
- If not found, note as CRITICAL issue
Meta Tags to Extract:
<meta name="theme-color" content="..."><meta name="apple-mobile-web-app-capable" content="..."><meta name="apple-mobile-web-app-status-bar-style" content="..."><meta name="mobile-web-app-capable" content="..."><meta name="viewport" content="...">(check forviewport-fit=cover)<meta http-equiv="Content-Security-Policy" content="..."><link rel="apple-touch-icon" href="..."><link rel="apple-touch-startup-image" ...>(iOS splash screens)
Step 3: Fetch Manifest
If manifest URL was found, use WebFetch to retrieve it:
WebFetch: {manifest_url}
Prompt: "Return the complete manifest.json content as raw JSON."
If manifest fetch fails (CORS, 404, etc.), score manifest categories as 0 and continue.
Step 4: Fetch Service Worker
If service worker URL was found, use WebFetch to retrieve it:
WebFetch: {sw_url}
Prompt: "Return the complete service worker JavaScript code."
If SW fetch fails, score SW-related categories as 0 and continue.
Step 5: Analyze & Score
Evaluate each category using the detailed checklist below. Track:
- Passed items (full points)
- Failed items (0 points) - record as issues
- Partial items (partial points where applicable)
Step 6: Generate Report
Output a markdown report following the template at the end of this document.
Detailed Scoring Checklist
Category 1: Manifest Compliance (20 points)
| Check | Points | How to Verify |
|---|---|---|
name field present and non-empty |
2 | manifest.name exists and length > 0 |
short_name present (≤12 chars recommended) |
2 | manifest.short_name exists |
icons array with 192x192 PNG |
4 | icons array has item with sizes="192x192" |
icons array with 512x512 PNG |
4 | icons array has item with sizes="512x512" |
start_url defined |
2 | manifest.start_url exists |
display mode set (standalone/fullscreen/minimal-ui) |
2 | manifest.display is one of allowed values |
background_color specified |
2 | manifest.background_color exists (hex/rgb/named) |
theme_color specified |
2 | manifest.theme_color exists |
Critical Blocker: If manifest is missing entirely, this category scores 0/20.
Category 2: Advanced Manifest Features (15 points)
| Check | Points | How to Verify |
|---|---|---|
description field present |
1 | manifest.description exists |
screenshots array for install UI |
2 | manifest.screenshots array with ≥1 item |
shortcuts array for quick actions |
2 | manifest.shortcuts array with ≥1 item |
categories array defined |
1 | manifest.categories exists |
orientation preference set |
1 | manifest.orientation exists |
dir and lang for i18n |
1 | manifest.dir OR manifest.lang exists |
id field for app identity |
1 | manifest.id exists |
scope properly defined |
1 | manifest.scope exists |
| Maskable icon present | 1 | icons array has item with purpose="maskable" or "any maskable" |
note_taking object |
1 | manifest.note_taking exists (ChromeOS lock screen notes) |
widgets array |
1 | manifest.widgets exists (Windows 11 Widgets Board) |
| iOS splash screens present | 2 | <link rel="apple-touch-startup-image"> tags for multiple device sizes |
iOS Splash Screen Note: iOS requires separate <link rel="apple-touch-startup-image"> tags for each device size. Without these, iOS shows a blank white screen during PWA launch. Check for multiple media queries covering different device dimensions.
Category 3: Service Worker & Caching (33 points)
| Check | Points | How to Verify |
|---|---|---|
| SW registered in HTML | 2 | navigator.serviceWorker.register() found |
install event handler present |
3 | SW contains addEventListener('install', ...) or self.oninstall |
activate event handler present |
3 | SW contains addEventListener('activate', ...) or self.onactivate |
fetch event handler present |
4 | SW contains addEventListener('fetch', ...) or self.onfetch |
| Cache API usage (caches.open/put/match) | 3 | SW contains caches.open or cache.put or cache.match |
| Cache versioning/naming strategy | 2 | SW has cache name variable (CACHE_NAME, CACHE_VERSION, etc.) |
| Old cache cleanup in activate | 2 | activate handler deletes old caches |
| Background Sync support | 2 | SW contains addEventListener('sync', ...) or addEventListener('periodicsync', ...) |
| Workbox usage (bonus, not required) | 1 | SW imports workbox or uses workbox.* methods |
skipWaiting() usage |
1 | SW contains self.skipWaiting() for instant activation |
clients.claim() usage |
1 | SW contains clients.claim() for immediate control |
| Navigation preload | 1 | SW uses navigationPreload.enable() |
| Stale-while-revalidate pattern | 1 | fetch handler serves cache then updates in background |
| Push event handler | 1 | SW contains addEventListener('push', ...) |
| notificationclick handler | 1 | SW contains addEventListener('notificationclick', ...) |
| Notification action buttons | 1 | Push notifications include actions array OR notificationclick checks event.action |
| Multiple caching strategies | 2 | SW uses different strategies for different routes (CacheFirst, NetworkFirst, StaleWhileRevalidate) |
| Cache expiration config | 1 | SW has maxEntries or maxAgeSeconds for cache pruning |
| SW message handler | 1 | SW contains addEventListener('message', ...) for client communication |
Critical Blocker: If no service worker, this category scores 0/33.
Caching Strategies Note: Production-grade PWAs should use different caching strategies based on resource type:
- CacheFirst: Static assets, fonts, images (rarely change)
- NetworkFirst: API responses, dynamic content (freshness matters)
- StaleWhileRevalidate: JS/CSS bundles (speed + freshness balance)
Look for patterns like
new CacheFirst(),new NetworkFirst(), or explicit strategy patterns in fetch handlers.
Cache Expiration Note: Without expiration limits, caches grow unbounded and can exceed storage quotas. Look for ExpirationPlugin with maxEntries or maxAgeSeconds, or custom cleanup logic in the fetch handler.
Category 4: Offline Capability (24 points)
| Check | Points | How to Verify |
|---|---|---|
| Offline fallback page defined | 3 | SW caches and serves an offline.html or similar |
| App shell resources precached | 3 | install event caches core HTML/CSS/JS files |
| Offline indicator in UI (code pattern) | 2 | Code checks navigator.onLine or listens to online/offline events |
| Network-first or cache-first strategy evident | 2 | fetch handler has clear strategy pattern |
| Update prompt shown to user | 1 | Code handles SW update with user notification (e.g., "New version available") |
| Graceful update flow | 1 | Update doesn't force reload without warning, user can choose when to update |
| Update state persistence | 1 | localStorage flag prevents update prompt re-appearing after update (e.g., pwa-just-updated) |
| Touch event double-fire prevention | 1 | Update/action handlers prevent duplicate execution from onClick + onTouchEnd |
| Persistent storage request | 1 | Code uses navigator.storage.persist() to prevent iOS data eviction |
| IndexedDB offline storage | 1 | Code uses indexedDB.open() or idb library for structured offline data |
| Storage quota monitoring | 1 | Code uses navigator.storage.estimate() for storage health checks |
| Background sync client trigger | 1 | Client triggers registration.sync.register() when coming back online |
| Periodic sync registration | 1 | Client registers registration.periodicSync.register() on app init |
| Visibility change reconnection | 2 | Code listens to visibilitychange event and triggers WebSocket/real-time reconnection when page becomes visible |
| Real-time reconnection strategy | 2 | WebSocket/Socket.io configured with sufficient reconnection attempts (>5 or Infinity) and progressive backoff |
| Connection room re-subscribe | 1 | After real-time reconnection, rooms/channels are re-joined automatically (not only on initial connect) |
Update UX Note: Good PWAs notify users when updates are available and let them choose when to apply the update. Look for patterns like useRegisterSW, workbox-window, or custom SW update handling with user-facing notifications.
Background Sync Client Trigger Note: Having a sync event handler in the service worker is not enough. The client must explicitly trigger background sync when coming back online by calling navigator.serviceWorker.ready.then(reg => reg.sync.register('sync-pending-requests')) in the online event listener. Without this, offline requests remain queued indefinitely.
Periodic Sync Registration Note: The service worker periodicsync event handler must be complemented by client-side registration during app initialization. Look for registration.periodicSync.register('sync-content', { minInterval: ... }) wrapped in a permission check (navigator.permissions.query({ name: 'periodic-background-sync' })). This enables automatic background content updates even when the app is closed.
Update State Note: After a user clicks "Update", the PWA reloads. Without state management, the update prompt may immediately re-appear because the new service worker is still "waiting". Use localStorage flags (e.g., pwa-just-updated with timestamp) to suppress the prompt for a brief period (30 seconds) after update completion. Also implement double-fire prevention for touch handlers - on iOS, both onClick and onTouchEnd may fire, causing duplicate updates.
Offline Storage Note: For complex PWAs with user-generated content, localStorage alone is insufficient. Use IndexedDB for structured data storage (images, generation history, preferences). Request persistent storage with navigator.storage.persist() to prevent iOS from evicting data after 7 days of inactivity. Monitor storage quota with navigator.storage.estimate() to warn users before running out of space.
Visibility Change Reconnection Note: When a PWA is minimized, tab-switched, or the user navigates to another app on mobile, WebSocket/real-time connections silently die. The browser fires visibilitychange when the user returns, but most apps don't listen for it. Without a visibilitychange handler that checks connection state and triggers reconnection, users return to a permanently disconnected app. This is especially critical on iOS where PWAs are effectively frozen when backgrounded. Look for:
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && !socket.connected) {
socket.connect(); // or custom reconnect logic
}
});
Real-Time Reconnection Strategy Note: Socket.io and similar libraries default to limited reconnection attempts (e.g., 5 attempts). In a PWA context where users may background the app for hours, 5 attempts are exhausted in ~15 seconds — long before the user returns. Production PWAs should configure reconnectionAttempts: Infinity with a reasonable reconnectionDelayMax (e.g., 30 seconds) for progressive backoff. Also provide a manual retry mechanism in the UI so users can trigger immediate reconnection. Look for:
reconnectionAttemptsconfiguration (should be high or Infinity)reconnectionDelayMaxconfiguration (should be >5000ms)- Manual reconnect button or tap-to-retry UI
reconnect_failedevent handling
Connection Room Re-Subscribe Note: After a WebSocket reconnection, the server-side room/channel memberships are lost. If the app only joins rooms on initial connection (e.g., in a useEffect([workspaceId]) that doesn't re-trigger on reconnect), real-time events stop flowing after reconnection. The fix is to listen for the connect event persistently and re-join rooms on every reconnection — not just the first connection. Look for patterns where room-joining logic is called inside a persistent connect event listener rather than a self-removing one.
Category 5: Installability Requirements (13 points)
| Check | Points | How to Verify |
|---|---|---|
| Served over HTTPS | 3 | URL starts with https:// |
| Valid manifest linked in HTML | 2 | exists with valid href |
| Service worker with fetch handler | 2 | Covered in SW category, cross-check |
| 192x192 icon present | 1 | Covered in manifest, cross-check |
| 512x512 icon present | 1 | Covered in manifest, cross-check |
apple-touch-icon for iOS |
1 | in HTML |
beforeinstallprompt handled |
2 | HTML/JS contains beforeinstallprompt event listener |
| Custom install UI | 1 | Code shows/hides custom install button |
Note: prefer_related_applications: true in manifest BLOCKS browser install prompt - flag as CRITICAL if found.
Category 6: Security Measures (16 points)
| Check | Points | How to Verify |
|---|---|---|
| HTTPS enforced | 2 | URL is https:// (duplicate check for emphasis) |
| Content Security Policy present | 3 | CSP meta tag or mention in SW/HTML |
| Subresource Integrity (SRI) on scripts | 2 |