Performance2025-01-038 min read

Progressive Web App Checklist: Build Better PWAs

A complete checklist for building Progressive Web Apps. Learn what makes a great PWA and how to implement PWA features correctly.

Introduction

Progressive Web Apps (PWAs) combine the best of web and mobile apps. This checklist covers everything you need to build a PWA that provides an app-like experience on the web.

What Makes a PWA?

A Progressive Web App must have:

  1. Service Worker for offline functionality
  2. Web App Manifest for installability
  3. Served over HTTPS for security
  4. Responsive Design for all devices

PWA Checklist

1. Web App Manifest

Essential manifest fields:

{
  "name": "My Progressive Web App",
  "short_name": "MyPWA",
  "description": "A description of your app",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#000000",
  "icons": [
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ]
}

Checklist:

  • [ ] Include name and short_name
  • [ ] Set appropriate display mode
  • [ ] Add theme_color and background_color
  • [ ] Provide multiple icon sizes (192x192 minimum)
  • [ ] Include maskable icon for adaptive icons
  • [ ] Set start_url (usually / or /?source=pwa)
  • [ ] Add description for better discoverability

2. Service Worker

Basic service worker setup:

// sw.js
const CACHE_NAME = "v1";
const urlsToCache = ["/", "/styles.css", "/script.js", "/offline.html"];

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(urlsToCache)),
  );
});

self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches
      .match(event.request)
      .then((response) => response || fetch(event.request)),
  );
});

Register service worker:

// main.js
if ("serviceWorker" in navigator) {
  navigator.serviceWorker
    .register("/sw.js")
    .then(() => console.log("Service Worker registered"))
    .catch((err) => console.log("SW registration failed", err));
}

Checklist:

  • [ ] Register service worker on page load
  • [ ] Cache shell (HTML, CSS, JS)
  • [ ] Implement fallback for offline
  • [ ] Handle different request types appropriately
  • [ ] Update cache strategy as needed
  • [ ] Clean up old caches in activate event

3. Cache Strategies

Cache First - for static assets:

self.addEventListener("fetch", (event) => {
  if (
    event.request.destination === "style" ||
    event.request.destination === "script" ||
    event.request.destination === "image"
  ) {
    event.respondWith(
      caches.match(event.request).then((response) => {
        return (
          response ||
          fetch(event.request).then((fetchResponse) => {
            return caches.open(CACHE_NAME).then((cache) => {
              cache.put(event.request, fetchResponse.clone());
              return fetchResponse;
            });
          })
        );
      }),
    );
  }
});

Network First - for API calls:

self.addEventListener("fetch", (event) => {
  if (event.request.url.includes("/api/")) {
    event.respondWith(
      fetch(event.request)
        .then((response) => {
          return caches.open(API_CACHE).then((cache) => {
            cache.put(event.request, response.clone());
            return response;
          });
        })
        .catch(() => caches.match(event.request)),
    );
  }
});

Stale While Revalidate - for content:

self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.match(event.request).then((cachedResponse) => {
      const fetchPromise = fetch(event.request).then((networkResponse) => {
        caches.open(CACHE_NAME).then((cache) => {
          cache.put(event.request, networkResponse.clone());
        });
        return networkResponse;
      });
      return cachedResponse || fetchPromise;
    }),
  );
});

4. Installability

Requirements for install prompt:

  • [ ] Service worker registered
  • [ ] Web app manifest present
  • [ ] Icon at least 192x192
  • [ ] Site served over HTTPS
  • [ ] Site visited at least twice (5 minutes apart)
  • [ ] No active dismiss of install prompt

Test installability:

window.addEventListener("beforeinstallprompt", (e) => {
  // Prevent Chrome 67 and earlier from automatically showing the prompt
  e.preventDefault();
  // Stash the event so it can be triggered later
  deferredPrompt = e;
  // Update UI to show install button
  showInstallButton();
});

5. Responsive Design

Viewport meta tag:

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

Responsive considerations:

  • [ ] Design for mobile-first
  • [ ] Test on multiple screen sizes
  • [ ] Ensure touch targets are at least 44x44px
  • [ ] Use flexible layouts (flexbox, grid)
  • [ ] Test on actual devices

6. Performance

Core Web Vitals for PWAs:

| Metric | Target | Importance | | ------ | ----------- | ---------- | | LCP | Under 2.5s | Critical | | FID | Under 100ms | Important | | CLS | Under 0.1 | Important | | TTI | Under 3.8s | Important |

Optimization checklist:

  • [ ] Optimize images (WebP, lazy loading)
  • [ ] Minify CSS and JavaScript
  • [ ] Use code splitting
  • [ ] Implement service worker caching
  • [ ] Preload critical resources
  • [ ] Defer non-critical scripts

7. Offline Functionality

Offline page:

<!-- offline.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>You're Offline</title>
    <style>
      body {
        font-family: system-ui;
        text-align: center;
        padding: 2rem;
      }
      h1 {
        color: #333;
      }
    </style>
  </head>
  <body>
    <h1>You're Offline</h1>
    <p>Please check your internet connection.</p>
    <button onclick="window.location.reload()">Try Again</button>
  </body>
</html>

Service worker offline fallback:

self.addEventListener("fetch", (event) => {
  event.respondWith(
    fetch(event.request).catch(() => caches.match("/offline.html")),
  );
});

8. Push Notifications

Request permission:

function requestNotificationPermission() {
  return new Promise((resolve, reject) => {
    if (!("Notification" in window)) {
      reject(new Error("Notifications not supported"));
      return;
    }

    if (Notification.permission === "granted") {
      resolve();
      return;
    }

    Notification.requestPermission().then((result) => {
      if (result === "granted") {
        resolve();
      } else {
        reject(new Error("Permission denied"));
      }
    });
  });
}

Subscribe to push:

async function subscribeToPush() {
  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: "YOUR_VAPID_PUBLIC_KEY",
  });
  // Send subscription to server
}

9. App Shell Architecture

Structure your PWA with:

index.html         # App shell (cached)
app.css            # Core styles (cached)
app.js             # App logic (cached)
content/           # Dynamic content (network first or stale-while-revalidate)

Benefits:

  • Instant loading on repeat visits
  • Consistent UI across pages
  • Better perceived performance
  • Simplified data management

10. Testing

PWA testing checklist:

  • [ ] Lighthouse PWA audit (score 90+)
  • [ ] Test offline functionality
  • [ ] Test on mobile devices
  • [ ] Test installation on different platforms
  • [ ] Test push notifications
  • [ ] Verify caching behavior
  • [ ] Test update flow

Lighthouse PWA criteria:

  • Fast and reliable
  • Installable
  • PWA optimized
  • Works across browsers

Framework-Specific PWA Setup

React (Create React App)

npm install workbox-webpack-plugin

CRA includes PWA support by default with react-scripts.

Next.js

// next.config.js
module.exports = {
  pwa: {
    dest: "public",
    register: true,
    skipWaiting: true,
  },
};

Vue (Vite)

npm install vite-plugin-pwa -D
// vite.config.js
import { VitePWA } from "vite-plugin-pwa";

export default {
  plugins: [
    VitePWA({
      registerType: "autoUpdate",
      manifest: {
        /* ... */
      },
    }),
  ],
};

PWA Best Practices

  1. Start Small: Begin with basic offline support
  2. Progressive Enhancement: Core features work without JS
  3. Regular Updates: Keep service worker updated
  4. Clear Communication: Tell users about offline status
  5. Test Thoroughly: Test on real devices and networks

Conclusion

Building a PWA requires attention to detail across multiple areas, but the result is a web experience that rivals native apps. Use this checklist to ensure your PWA meets all the requirements for installability and performance.

Scan your website to check if your site meets PWA requirements and get specific recommendations.

Scan Your Website Now

Get comprehensive insights into your website's technology, security, SEO, and performance.

You Might Also Like