To make your website work offline, register a service worker over HTTPS, cache your key files when it installs, and answer failed page loads with a cached offline page. The service worker sits between your pages and the network, so when the connection drops it can serve files from its cache instead. A small site needs one short script and one fallback page.
What a service worker does for offline use
A service worker is a script the browser runs apart from your page, and for offline use its job is to intercept requests and answer them from a cache. MDN’s guide explains that a fetch event fires every time any resource controlled by a service worker is fetched. Your code decides whether that request goes to the network, the cache, or both.
The cache itself is the Cache Storage API, which web.dev’s caching lesson describes as the store your service worker manages. Three events matter:
- install: download and cache the files you need offline.
- fetch: answer each request, from the network or the cache.
- activate: delete caches from older versions.
What you need first
A service worker needs HTTPS and a file at the root of your site. MDN’s API page says service workers are only available in secure contexts, although browsers also treat http://localhost as secure for local development.
Put sw.js at the root. MDN notes the default scope is the directory where the worker file sits, so a worker at /js/sw.js can’t control /index.html.
How to set up service worker offline support, step by step
A basic service worker offline setup has three parts: register the worker, precache an offline page and core files, and serve the offline page when a page load fails. This version follows web.dev’s offline fallback pattern.
1. Register the worker in your main script:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', { scope: '/' });
}
2. Create offline.html. Keep it self-contained, with inline CSS and no external fonts. The web.dev article warns that you need to cache every resource the offline page uses.
3. Write sw.js:
const CACHE = 'site-v1';
const FILES = ['/', '/offline.html', '/styles.css', '/app.js'];
self.addEventListener('install', (event) => {
event.waitUntil(caches.open(CACHE).then((c) => c.addAll(FILES)));
});
self.addEventListener('activate', (event) => {
event.waitUntil(caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))
));
});
self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => caches.match('/offline.html'))
);
return;
}
event.respondWith(
caches.match(event.request).then((hit) => hit || fetch(event.request))
);
});
Pages try the network first and fall back to offline.html. Static files come from the cache first. If one file in FILES fails to download, addAll() changes nothing, and MDN says a rejected install means the worker won’t do anything. Check every path.
4. Test it. Load the site once online, switch the browser to offline, and reload. You should see your pages or the offline page, not the browser’s error screen.
Which caching strategy to use for each file
The right caching strategy depends on how often a file changes and how bad stale content would be. web.dev’s serving lesson defines the common ones:
| Strategy | What it does | Good for |
|---|---|---|
| Cache first | Cache, then network if missing | Versioned CSS, JS, fonts, images |
| Network first | Network, then cache if it fails | HTML pages, news, prices |
| Stale while revalidate | Serves the cache, refreshes it in the background | Avatars, non-critical API data |
| Network only | Never uses the cache | Payments, sign-in |
| Cache only | Never uses the network; files must be cached first | The offline page itself |
My take: use network first for HTML. Cache-first pages feel fast, but a visitor can be stuck on yesterday’s version long after you’ve fixed a typo or a price.
Why users keep seeing old files
Users see old files because the cache doesn’t refresh itself. web.dev’s caching lesson is blunt: the Cache Storage API doesn’t update your assets if you change them on your server, and it doesn’t delete them either.
Two habits fix most of it:
- Rename the cache on every deploy, for example
site-v1tosite-v2. Theactivatehandler above then deletes the old one. - Know when the new worker takes over. MDN explains that a new version waits until every page controlled by the old one has closed. Calling
skipWaiting()activates it without waiting, at the cost of swapping files under a page that’s already open.
The error case most tutorials miss
A failed page load is not always a network error. The web.dev fallback code carries a comment that matters: if fetch() returns a 4xx or 5xx response, the catch() is not called. Your server’s error page reaches the user even though offline.html is sitting in the cache.
If you want the offline page for server errors too, check response.ok inside the then() and return the cached page when it’s false. The Workbox fallback guide offers a ready-made recipe, setDefaultHandler(new NetworkOnly()); offlineFallback();, which assumes the file is named offline.html at the site root.
If you plan to publish a PWA on Google Play, Chrome’s quality criteria count a 404 or 5xx inside the app as a failure.
Limits of a service worker offline cache
A service worker cache is temporary storage the browser can clear. MDN’s storage page lists the rules:
- Data is best-effort by default.
navigator.storage.persist()asks the browser to keep it. - Chromium lets an origin use up to 60% of the total disk size.
- Safari, with tracking prevention on, deletes script-created data for an origin with no user interaction in the last seven days of browser use.
- Going over quota throws a
QuotaExceededError.
The real limitation: offline only works after one online visit. A first-time visitor with no signal gets nothing, because the worker hasn’t installed yet.
When bundling HTML in an app is simpler
If your content is a few self-contained pages, such as a menu, a manual or a price list, packing them into an Android app avoids the first-visit and eviction problems. The files ship inside the APK, so they open offline from the first launch. The PWA vs WebView app comparison covers the trade-off, and whether a WebView app works without internet explains which parts need a connection.
WebToAPK Free can bundle self-contained HTML or text sections this way. The limit: its importer takes one HTML file up to 100 KB, with no ZIP or folder imports, and external fonts, scripts and APIs still need a connection. The offline HTML app guide shows the setup.
FAQ
Does a service worker work on plain HTTP?
No. Service workers only run in secure contexts, which means HTTPS. Browsers treat http://localhost as secure so you can test on your own machine.
Why do my users still see old files after I deploy?
The Cache Storage API doesn’t update or delete cached files when you change them on the server. Rename your cache when you deploy and delete the old one in the activate event.
Should I use Workbox or write the service worker myself?
For a handful of files, a hand-written worker is short and easy to read. For a site with a build step and many assets, Workbox generates the precache list and handles fallbacks for you.
Can the browser delete my offline cache?
Yes. Storage is best-effort by default, and Safari deletes script-created data for an origin with no user interaction in the last seven days of browser use when tracking prevention is on.