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:
- Service Worker for offline functionality
- Web App Manifest for installability
- Served over HTTPS for security
- 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
nameandshort_name - [ ] Set appropriate
displaymode - [ ] Add
theme_colorandbackground_color - [ ] Provide multiple icon sizes (192x192 minimum)
- [ ] Include maskable icon for adaptive icons
- [ ] Set
start_url(usually/or/?source=pwa) - [ ] Add
descriptionfor 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
- Start Small: Begin with basic offline support
- Progressive Enhancement: Core features work without JS
- Regular Updates: Keep service worker updated
- Clear Communication: Tell users about offline status
- 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.