"Works offline" is the promise that makes people take web apps seriously, and the promise most often broken by caching everything or nothing. This guide is about deciding, file by file, what your service worker should hold.
Three buckets
1. Precache: the shell
Files without which the app cannot render at all, cached during the service worker's install event:
- The start page HTML (or the app shell if you are a single-page app)
- Core CSS and JS bundles
- The logo and the 192 px icon
- Web fonts you self-host
offline.html
Rule: if the total is over about 1–2 MB, you are precaching too much. Everything else can wait until it is requested.
2. Runtime cache: things people actually use
Cached the first time they are requested, with the strategy matching their volatility:
- Pages the user visits → network first (fresh when online, available when not)
- Images → stale-while-revalidate, with a size cap (e.g. keep the 60 most recent)
- Product data, menus, schedules fetched as JSON → network first with a short-lived cache
- Third-party scripts you must have (payments, maps) → don't cache; they change under you
3. Never cache
- Anything non-GET
- Authenticated or personalised responses (cart, account, dashboards) unless the strategy is network first and the fallback is an explicit "you're offline" state
- Analytics and ad requests — caching them breaks measurement and violates network terms
- Streaming media (range requests don't play well with the Cache API without extra work)
Storage quota and eviction
Browsers grant an origin a share of free disk space — commonly hundreds of megabytes on phones, more on desktop — and may evict the whole origin's storage under pressure, oldest-unused first. Two implications:
- Don't treat the cache as permanent. Anything the user created (drafts, saves) belongs in IndexedDB, and even that should sync to your server when online.
- Ask for persistence if the app is one people rely on:
navigator.storage.persist()requests that the origin be exempt from eviction. Chrome grants it automatically for installed apps with engagement; Firefox prompts the user.
Check usage with navigator.storage.estimate() and show a friendly message if you are near the limit rather than failing silently.
Versioning without stranding users
When you deploy, the old service worker keeps running until every tab of the old version is closed — a user with the app pinned open can stay on old code for days. Options, from gentle to forceful:
- Let it happen. Fine for content sites; they get the update next open.
- Show a "new version available — refresh" banner when a new worker is waiting (
registration.waiting), and callskipWaiting()+ reload on click. Best user experience for apps. skipWaiting()on install unconditionally. The new worker takes over immediately; pages loaded with old assets may break until refreshed. Only if your assets are hash-named so old pages keep working.
Whatever you choose, bump the cache name every deploy and delete old caches on activate, or storage fills with stale versions.
Designing the offline page
The generated offline.html is a start. Make it useful:
- Say what happened in normal words: "You're offline. This page isn't saved on your device yet."
- Offer what is available: links to cached sections, or a list of recently viewed pages read from the cache with
caches.open().keys(). - Auto-retry: listen for the
onlineevent and reload. - Keep it self-contained — inline its CSS and use the cached logo, because nothing else may load.
Offline forms
If users submit things (orders, comments, check-ins), the honest offline behaviour is to queue: store the submission in IndexedDB, show "Saved — will send when you're back online", and send it from the page when online fires. Background Sync automates the send on Chromium browsers but is not available on iOS, so build the page-side retry regardless.
Testing
- DevTools → Network → Offline, then navigate: precached pages load, others show your offline page.
- Aeroplane mode on a real phone from the installed app — the only test that catches OS-level behaviour.
- Deploy a trivial change and confirm the update path you chose actually delivers it.
- Fill the cache (browse widely) and confirm your size cap trims images.
- Clear site data and confirm the first load still works — the service worker must never be required for the first visit.