Progressive Web Apps combine the best of websites and native apps. They load like web pages but offer app-like features: they work offline, can be installed on the home screen, send push notifications, and launch in their own window. Companies like Twitter, Pinterest, and Starbucks have seen dramatic improvements in engagement and conversion after switching to PWAs.
What Makes a PWA?
A PWA is any website that meets three criteria:
- HTTPS: Served over a secure connection (required for service workers)
- Service Worker: A JavaScript file that runs in the background, handling caching and offline support
- Web App Manifest: A JSON file that tells the browser how to display the app when installed
Web App Manifest
{
"name": "My Awesome App",
"short_name": "MyApp",
"description": "A progressive web application",
"start_url": "/",
"display": "standalone",
"background_color": "#0f172a",
"theme_color": "#6366f1",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/icons/icon-512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}
<link rel="manifest" href="/manifest.json"> <meta name="theme-color" content="#6366f1">
Service Workers
A service worker is a JavaScript file that runs in a separate thread from your main page. It intercepts network requests, manages a cache, and can serve content when the user is offline.
// In your main app.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const reg = await navigator.serviceWorker.register('/sw.js');
console.log('SW registered:', reg.scope);
} catch (err) {
console.error('SW registration failed:', err);
}
});
}
const CACHE_NAME = 'v1';
const ASSETS = [
'/',
'/css/style.css',
'/js/app.js',
'/offline.html',
];
// Install — cache core assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(ASSETS))
.then(() => self.skipWaiting())
);
});
// Activate — clean old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys
.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
)
)
);
});
// Fetch — serve from cache, fall back to network
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(cached => cached || fetch(event.request))
.catch(() => caches.match('/offline.html'))
);
});
Caching Strategies
- Cache First: Check cache first, fall back to network. Best for static assets (CSS, JS, images).
- Network First: Try network first, fall back to cache. Best for API data and HTML pages.
- Stale While Revalidate: Serve from cache immediately, then update the cache from the network in the background. Best for frequently-updated content.
- Network Only: Always fetch from network. For non-cacheable requests (POST, authenticated APIs).
Making It Installable
When your PWA meets the install criteria (HTTPS, manifest, service worker), browsers will show an install prompt. You can also trigger it programmatically:
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
document.getElementById('install-btn').style.display = 'block';
});
document.getElementById('install-btn').addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(`User ${outcome === 'accepted' ? 'installed' : 'dismissed'} the PWA`);
deferredPrompt = null;
});
Testing Your PWA
- Chrome DevTools → Application tab — inspect manifest, service worker, cache storage
- Lighthouse audit — run a PWA audit to check all criteria
- Offline testing — toggle "Offline" in DevTools Network tab
Try Our Free Performance Tools
Minify your JavaScript and CSS for faster PWA loading.