What is a PWA?
A Progressive Web App is a website that behaves like an installed app: it works offline, loads instantly on repeat visits and can be added to the home screen or desktop. There is no single technology called a PWA — it is a set of capabilities you adopt incrementally.
Three ingredients make it work:
- HTTPS, so the browser trusts your origin enough to allow a service worker.
- A web manifest that describes the app’s name, icons and display mode.
- A service worker, a background script that intercepts network requests and manages caches.
The result is an experience that feels native while remaining a website you can update instantly.
The web manifest
The manifest is a JSON file linked from the HTML head. It tells the platform how to present the app when installed.
{
"name": "hackweb Reader",
"short_name": "Reader",
"start_url": "/",
"display": "standalone",
"background_color": "#0b0d0c",
"theme_color": "#0b0d0c",
"icons": [
{ "src": "/icons/192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/512.png", "sizes": "512x512", "type": "image/png" }
]
}
<!-- index.html -->
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#0b0d0c" />
display: "standalone" hides the browser chrome when installed, and the icons and colours control the splash screen and task switcher. A valid manifest plus a service worker is what makes the browser offer to install the app.
The service worker lifecycle
A service worker is a separate script with no DOM access. It has a distinct lifecycle: install, activate and fetch.
// register.js
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("/sw.js");
});
}
During install you pre-cache the app shell. During activate you clean up old caches. During fetch you intercept requests and decide how to respond. A new worker waits until all pages using the old one are closed before activating, which is why updates sometimes need a second reload.
Caching strategies
The heart of a service worker is its caching strategy. Different resources want different treatment.
- Cache-first — serve from cache, fall back to the network. Ideal for versioned, immutable assets like hashed bundles.
- Network-first — try the network, fall back to cache. Good for HTML and data that must be fresh.
- Stale-while-revalidate — serve the cached version immediately and update it in the background. Great for data where slightly stale is acceptable.
- Cache-only and network-only — for specific edge cases.
// strategies.js
self.addEventListener("fetch", (event) => {
const { request } = event;
if (request.destination === "image") {
event.respondWith(
caches.match(request).then((cached) => cached || fetch(request)),
);
}
});
The request.destination property tells you what kind of resource is being fetched, which lets you apply a strategy per asset type.
Offline fallback
Even with caching, some requests will fail. Provide a graceful fallback.
// fallback.js
self.addEventListener("fetch", (event) => {
if (event.request.mode === "navigate") {
event.respondWith(
fetch(event.request).catch(() => caches.match("/offline.html")),
);
}
});
An offline page, a cached shell or a clear “you are offline” state is far better than the browser’s default error. Combined with background sync, requests that failed offline can be retried automatically when connectivity returns.
Updates and versioning
Caching is powerful and easy to get wrong. The rule is simple: version your caches and delete old ones on activate. Otherwise users can be stuck on stale files indefinitely.
// activate.js
const CACHE = "app-v2";
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))),
),
);
});
For anything beyond the basics, the Workbox library handles precaching, routing, expiration and cleanup with well-tested defaults, and integrates with most build tools.
Beyond offline
Service workers enable more than caching:
- Background sync retries failed requests when the network returns.
- Push notifications deliver messages even when the app is closed.
- Periodic sync refreshes content in the background.
- Share target lets your app receive shared content from the OS.
Each requires user permission and should be used sparingly, but they are what make a PWA feel like a first-class app.
Best practices
- Serve over HTTPS and register the worker after load.
- Version caches and delete old ones on activate.
- Use cache-first for hashed assets and network-first for HTML.
- Always provide an offline fallback.
- Precache only the app shell, not the entire site.
- Consider Workbox instead of hand-writing complex strategies.
- Test offline mode and updates before shipping.
Common mistakes
- Caching HTML forever and serving stale pages.
- Forgetting to clean up old caches after a deploy.
- Caching opaque cross-origin responses without understanding their limits.
- Precaching everything and bloating the first install.
- Assuming the new worker activates immediately.
- Ignoring the storage quota and letting caches grow unbounded.
Where to go next
A service worker is the biggest single performance win for repeat visits and the foundation of offline support. Combine it with the techniques in the Web Performance guide, understand the HTTP requests it intercepts, and serve the app from a Node.js backend. Then add a manifest and a simple cache-first worker to an existing site and watch it work offline.