pwa-development · git:20251218.8e57ad2 · 2025-12-18 · sha256 d69658a00ec4df9b
pwa-development git:20251218.8e57ad2B
Immutable. This exact content never changes and is served at /api/v1/blob/d69658a00ec4df9b.
---
name: pwa-development
description: Progressive Web App development for installable, offline-capable web applications. Use when building PWAs, implementing service workers, or creating offline-first experiences.
---
# Progressive Web App (PWA) Development
Build installable, offline-capable web applications that work across all platforms.
## PWA Capabilities
| Feature | Support |
|---------|---------|
| Offline access | All modern browsers |
| Install prompt | Chrome, Edge, Samsung |
| Push notifications | All except iOS Safari |
| Background sync | Chrome, Edge |
| File handling | Chrome, Edge |
| Share target | Chrome, Edge, Safari |
---
## Core Requirements
### 1. Web App Manifest
```json
// public/manifest.json
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"description": "A progressive web application",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3b82f6",
"orientation": "portrait-primary",
"scope": "/",
"icons": [
{
"src": "/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"screenshots": [
{
"src": "/screenshots/desktop.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
},
{
"src": "/screenshots/mobile.png",
"sizes": "750x1334",
"type": "image/png",
"form_factor": "narrow"
}
],
"shortcuts": [
{
"name": "New Document",
"short_name": "New",
"url": "/new",
"icons": [{ "src": "/icons/new.png", "sizes": "96x96" }]
}
],
"share_target": {
"action": "/share",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url",
"files": [
{
"name": "files",
"accept": ["image/*", "text/*"]
}
]
}
}
}
```
### 2. HTML Meta Tags
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- PWA Meta Tags -->
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#3b82f6">
<meta name="description" content="My Progressive Web App">
<!-- iOS Specific -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="MyPWA">
<link rel="apple-touch-icon" href="/icons/icon-192x192.png">
<!-- Windows Specific -->
<meta name="msapplication-TileColor" content="#3b82f6">
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png">
<title>My PWA</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
```
---
## Service Worker
### Basic Service Worker
```typescript
// public/sw.js
const CACHE_NAME = 'my-pwa-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/manifest.json',
'/icons/icon-192x192.png',
];
// Install - cache static assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(STATIC_ASSETS);
})
);
self.skipWaiting();
});
// Activate - clean old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
})
);
self.clients.claim();
});
// Fetch - serve from cache, fallback to network
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
// Cache-first strategy
if (cached) {
return cached;
}
// Network fallback
return fetch(event.request).then((response) => {
// Don't cache non-GET or failed requests
if (event.request.method !== 'GET' || !response.ok) {
return response;
}
// Cache successful responses
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
return response;
});
})
);
});
```
### Workbox (Recommended)
```typescript
// src/sw.ts
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import {
CacheFirst,
NetworkFirst,
StaleWhileRevalidate,
} from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
// Precache static assets
precacheAndRoute(self.__WB_MANIFEST);
// Cache images
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new ExpirationPlugin({
maxEntries: 100,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
}),
],
})
);
// Network-first for API calls
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api-cache',
plugins: [
new ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 5 * 60, // 5 minutes
}),
],
})
);
// Stale-while-revalidate for pages
registerRoute(
({ request }) => request.mode === 'navigate',
new StaleWhileRevalidate({
cacheName: 'pages',
})
);
```
### Service Worker Registration
```typescript
// src/registerSW.ts
export async function registerServiceWorker() {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/',
});
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
if (newWorker) {
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// New update available
dispatchEvent(new CustomEvent('sw:update'));
}
}
});
}
});
console.log('Service Worker registered:', registration.scope);
} catch (error) {
console.error('Service Worker registration failed:', error);
}
}
}
```
---
## Install Prompt
### Custom Install Button
```tsx
import { useState, useEffect } from 'react';
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}
function InstallButton() {
const [deferredPrompt, setDeferredPrompt] =
useState<BeforeInstallPromptEvent | null>(null);
const [isInstalled, setIsInstalled] = useState(false);
useEffect(() => {
// Check if already installed
if (window.matchMedia('(display-mode: standalone)').matches) {
setIsInstalled(true);
return;
}
const handler = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e as BeforeInstallPromptEvent);
};
window.addEventListener('beforeinstallprompt', handler);
return () => window.removeEventListener('beforeinstallprompt', handler);
}, []);
const handleInstall = async () => {
if (!deferredPrompt) return;
await deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
setIsInstalled(true);
}
setDeferredPrompt(null);
};
if (isInstalled || !deferredPrompt) return null;
return (
<button onClick={handleInstall} className="install-button">
Install App
</button>
);
}
```
---
## Offline Support
### Offline Detection
```tsx
import { useState, useEffect } from 'react';
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
// Usage
function App() {
const isOnline = useOnlineStatus();
return (
<div>
{!isOnline && (
<div className="offline-banner">
You're offline. Some features may be unavailable.
</div>
)}
</div>
);
}
```
### Background Sync
```typescript
// In Service Worker
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-data') {
event.waitUntil(syncData());
}
});
async function syncData() {
const db = await openDB('pending-requests', 1);
const requests = await db.getAll('requests');
for (const request of requests) {
try {
await fetch(request.url, request.options);
await db.delete('requests', request.id);
} catch {
// Will retry on next sync
}
}
}
// Register sync from app
async function queueRequest(url: string, options: RequestInit) {
await navigator.serviceWorker.ready;
if ('sync' in window.ServiceWorkerRegistration.prototype) {
// Store request and trigger sync
const db = await openDB('pending-requests', 1);
await db.add('requests', { url, options, id: Date.now() });
await navigator.serviceWorker.ready.then((reg) =>
reg.sync.register('sync-data')
);
} else {
// Fallback to immediate fetch
await fetch(url, options);
}
}
```
---
## Push Notifications
### Request Permission
```typescript
async function requestNotificationPermission() {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
});
// Send subscription to server
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
});
}
}
```
### Handle Push in Service Worker
```typescript
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? {};
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/icon-192x192.png',
badge: '/icons/badge.png',
data: data.url,
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.notification.data) {
event.waitUntil(
clients.openWindow(event.notification.data)
);
}
});
```
---
## Vite PWA Plugin
```typescript
// vite.config.ts
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.ico', 'robots.txt', 'icons/*.png'],
manifest: {
name: 'My PWA',
short_name: 'MyPWA',
theme_color: '#3b82f6',
icons: [
// ... icon definitions
],
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24, // 24 hours
},
},
},
],
},
}),
],
});
```
---
## Testing PWA
### Lighthouse Audit
1. Open Chrome DevTools
2. Go to Lighthouse tab
3. Select "Progressive Web App"
4. Run audit
### Required Scores
- Performance: 90+
- PWA: 100
- Accessibility: 90+
- Best Practices: 90+
---
## Best Practices
### DO:
- Use HTTPS (required)
- Provide offline fallback
- Cache static assets
- Show update notification
- Handle all screen sizes
### DON'T:
- Cache everything forever
- Block app on SW update
- Ignore iOS limitations
- Skip manifest icons
- Forget offline states