Progressive Web Apps: An Introduction
Learn what makes an app a PWA: service workers, manifests, install prompts, and offline-first strategies that turn a website into something that feels native.
What you'll learn
- ✓What turns a site into a PWA
- ✓Service worker lifecycle
- ✓Caching strategies for offline
- ✓Web App Manifest essentials
- ✓When a PWA is the right choice
Prerequisites
- •Familiar with HTTP
What and Why
A Progressive Web App is a website that uses modern browser APIs to behave like an installed application: it can be added to the home screen, launch full-screen, work offline, receive push notifications, and update without an app store.
PWAs are not a framework. They are a set of capabilities you opt into. Done well, they reduce friction (no install, no store review) while feeling close to native. Done poorly, they are slow websites with a manifest.
Mental Model
Three pillars make a PWA:
- A served-over-HTTPS site with reliable performance.
- A Web App Manifest describing how it installs.
- A service worker that intercepts network requests for caching and offline.
The service worker is the engine. It runs in its own thread, has no DOM, and persists across page loads. Once registered, it sits between your page and the network.
+-----------+ fetch +-----------------+ fetch +---------+
| Page JS | ---------> | Service Worker | --------> | Network |
+-----------+ +-----------------+ +---------+
^ |
| v
| +-----------------+
+----------------| Cache Storage |
+-----------------+
Cache hit returns instantly. Cache miss falls through to network. This intercept lets you design offline behavior, instant page loads, and background sync.
Hands-on Example
A minimal manifest.webmanifest:
{
"name": "Notes",
"short_name": "Notes",
"start_url": "/",
"display": "standalone",
"background_color": "#0b0b0b",
"theme_color": "#0b0b0b",
"icons": [
{ "src": "/icons/192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/512.png", "sizes": "512x512", "type": "image/png" }
]
}
Link it from your HTML head:
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#0b0b0b" />
Register a service worker:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
A simple cache-first strategy in sw.js:
const CACHE = 'app-v3';
const ASSETS = ['/', '/styles.css', '/app.js', '/offline.html'];
self.addEventListener('install', e => {
e.waitUntil(caches.open(CACHE).then(c => c.addAll(ASSETS)));
});
self.addEventListener('activate', e => {
e.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))
)
);
});
self.addEventListener('fetch', e => {
e.respondWith(
caches.match(e.request).then(hit =>
hit || fetch(e.request).catch(() => caches.match('/offline.html'))
)
);
});
Pick a strategy per resource:
- Static assets: cache-first.
- API GETs: stale-while-revalidate.
- User-mutating POSTs: network-only, queued with Background Sync if offline.
- HTML shell: network-first with a cached fallback.
Common Pitfalls
Cache-first on everything. Your users see stale UI for weeks. Version your cache key and invalidate aggressively.
Forgetting skipWaiting and clients.claim. New service workers wait until all tabs close. During development this causes endless confusion.
Shipping a service worker without a kill switch. A buggy SW can brick your site for returning users. Always have a no-op SW you can deploy to clear caches and unregister.
Treating PWAs as iOS-equivalent to native. Safari supports PWAs but with quirks: no push notifications on iOS for a long time, storage eviction, no background sync. Test on iOS specifically.
Ignoring the install prompt UX. The beforeinstallprompt event needs to be captured and shown contextually, not on first paint.
Caching cross-origin opaque responses. They take full storage quota each. Audit what you cache.
Practical Tips
- Use Workbox unless you have a strong reason not to. It handles the boring parts (versioning, routing, strategies) safely.
- Pre-cache only the app shell. Lazy-cache the rest as the user visits routes.
- Audit with Lighthouse. It catches missing manifest fields, icons, and HTTPS issues.
- Show a clear “back online” or “offline mode” indicator. Silent failures feel like bugs.
- Use
Cache-Controlheaders correctly even with a service worker; they still drive the network fetch path. - Keep your service worker file small and cache it with a short max-age. It is your update mechanism; you do not want it stuck.
- Test updates by toggling “Update on reload” in DevTools.
Wrap-up
A PWA is a deployment strategy plus a few APIs, not a rewrite. Add a manifest, register a service worker, and pick caching strategies that match your data freshness needs.
Start small: get the install prompt working and a basic offline page. Grow from there. The first time a user opens your app on the subway with no connection and it just works, the value clicks.
Related articles
- Web Service Workers: A Deep Dive
How service workers actually run: lifecycle, fetch interception, caching strategies, updates, and the operational habits that keep them from breaking your site.
- Web Content Security Policy Explained
A practical tour of CSP: what each directive does, how nonces and hashes work, how to roll out a policy safely with report-only mode, and the mistakes that quietly weaken it.
- Web Cookies vs localStorage vs sessionStorage
Compare the three main browser storage mechanisms by lifecycle, capacity, security, and use case so you pick the right tool for tokens, preferences, and state.
- Web CORS Explained In Depth
Understand Cross-Origin Resource Sharing: simple vs preflight requests, credentials, and the headers that decide whether your browser will let your fetch succeed.