Skip to content
Advanced17 min study

Service workers, caching, and offline support

Intercept requests with a service worker and choose a caching strategy per resource to support offline use without serving stale data.

Question progress0 / 10 completed
Start the lesson
Service workers, caching, and offline support visual explanation

WHAT YOU WILL BE ABLE TO DO

Learning outcomes

  • Explain service workers, caching, and offline support in plain language.
  • Connect the behavior to the underlying browser or framework model.
  • Implement the core pattern and reason through edge cases.
  • Answer common follow-ups without relying on memorized phrases.

01Explain it simply

A service worker is a script the browser runs separately from any page, sitting between your app and the network, that can intercept requests and decide whether to answer from a cache, the network, or both — which is what makes offline support and instant repeat loads possible.

Think of it like this: think of it like a personal assistant standing at your front door who intercepts every delivery request before it leaves the house: for some requests, the assistant already has what's needed in a drawer nearby — the cache — and hands it over instantly; for others, they still have to call the shop — the network. You write the instructions for which behavior applies to which kind of request.

One-line definition: Intercept requests with a service worker and choose a caching strategy per resource to support offline use without serving stale data.

02Mental model

A service worker runs on its own thread with no DOM access, goes through an install, activate, and idle/fetch lifecycle independent of any one page, and can outlive the page that registered it. Once active, it can intercept every fetch event from pages under its scope and choose a strategy: cache-first, serving from the Cache API and falling back to the network; network-first, trying the network and falling back to cache; or stale-while-revalidate, serving the cache immediately and updating it in the background for next time.

03Step by step

  • Register the service worker from the page, scoped to the routes it should control.
  • On install, open a named cache and pre-cache the app shell needed to boot offline.
  • On activate, delete old cache versions so stale assets don't linger.
  • In the fetch handler, choose a strategy per request type — cache-first for the app shell, network-first or stale-while-revalidate for data that should stay current.
  • Version the cache name so a new deploy can safely replace old entries.
  • Handle the update lifecycle deliberately, with skipWaiting/clients.claim or a user-facing refresh prompt, instead of leaving users on a stale worker indefinitely.

04Working example

JavaScript
// sw.jsconst CACHE = "shell-v3";self.addEventListener("install", (event) => {  event.waitUntil(caches.open(CACHE).then((c) => c.addAll(["/", "/app.js", "/app.css"])));});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) => {  event.respondWith(caches.match(event.request).then((cached) => cached ?? fetch(event.request)));});

install pre-populates one named cache with the app shell; activate clears any previous cache version so an old deploy can't leak stale files; fetch answers from that cache on a match and only touches the network otherwise — a minimal cache-first strategy suitable for a static shell, not for data that must stay fresh.

05Where it is used

  • Installable, offline-capable web apps (PWAs)
  • Instant repeat page loads by serving the app shell from cache
  • Background sync of writes made while offline
  • Push notifications, which require an active service worker registration

06Common mistakes

  • Cache-first for API or data endpoints, which serves stale data indefinitely instead of ever refetching
  • Forgetting to bump the cache name or version, so activate never clears old assets and storage grows unbounded
  • Assuming a new service worker takes over immediately, when by default it waits until every tab using the old one is closed
  • Caching opaque cross-origin responses without realizing their status can't be inspected, only replayed

07Interview answer

How to say it out loud: "A service worker runs independently of any page, with no DOM access, and once it's activated it can intercept every network request from pages in its scope through the fetch event — that's the hook that makes offline support possible, since instead of always hitting the network I can answer from the Cache API. The strategy has to match the resource: a static app shell can be cache-first since it rarely changes, but account or pricing data needs network-first or stale-while-revalidate so users aren't stuck looking at stale numbers. The piece people forget is lifecycle — caches need to be versioned and cleaned up on activate, and how aggressively a new worker takes over from an old one has to be a deliberate decision, or users get stuck on stale code after a deploy."

Name the specific caching strategy per request type instead of saying 'it caches things' — cache-first, network-first, and stale-while-revalidate solve different freshness and availability trade-offs, and applying the wrong one to a resource is the actual failure mode being probed for.

A user reports seeing old content after a deploy fixed it, while the service worker's fetch handler is cache-first for everything. What's the likely cause, and the fix?

The cache-first strategy is serving a previously cached response indefinitely because the cache version wasn't bumped and the new worker may not have activated yet; the fix is versioning the cache name per deploy, deleting old versions on activate, and deciding deliberately how new workers take control rather than leaving it to the default wait-for-tabs-to-close behavior.

DDConcept deep dives

Deep dive 1

A service worker sits in front of the network, not in front of the DOM

Unlike a regular script tag, a service worker runs on a separate thread with no access to window, document, or any page's DOM; what it does have is the ability to intercept every fetch request from pages within its registered scope, and to keep running, and receive events like push, even when no tab using it is open. That combination — network interception without page access, and a lifetime independent of any single page — is what makes it the right place to implement caching and offline behavior, and the wrong place to try to manipulate UI directly.

  • Communication with a controlled page happens through postMessage or the Clients API, not direct references.
  • A service worker can be evaluated and running before any page that uses it has finished loading.
  • Its scope, the set of URLs it controls, is determined by default by where the script file is served from.

Deep dive 2

Caching strategy is a per-resource decision, not a global setting

Cache-first, network-first, and stale-while-revalidate all answer the fetch handler's core question, cache or network and in what order, differently, and a real application typically needs more than one of them active at once, branched by request type inside a single fetch listener. The static app shell fits cache-first; live data that should be current when possible fits network-first or stale-while-revalidate. Applying one strategy to every request either serves stale data indefinitely or defeats the point of caching for offline use.

  • Branch the fetch handler's strategy on the request's URL or destination, not one global rule.
  • Never cache-first anything where staleness has real consequences, such as pricing or account balances.
  • Stale-while-revalidate is a good default for data where fast now and correct soon is an acceptable trade-off.

Deep dive 3

The update lifecycle needs a deliberate policy, not the default left in place

By default, a newly activated service worker doesn't take over pages that are already open, and a page keeps running against whichever worker version it started with — this avoids a page suddenly having its in-flight resources served by different code mid-session, but it also means users can sit on a stale version indefinitely if they never fully close and reopen the app. Calling skipWaiting() and clients.claim() forces immediate takeover, which needs to be paired with detecting the change so the page can reload cleanly, or it can serve a version-mismatched app.

  • Version the cache name per deploy and delete old versions in activate, unconditionally.
  • Decide explicitly between prompting the user to refresh and forcing takeover plus reload — don't leave it implicit.
  • Test the update path itself, not just the fresh-install path; most production bugs live in the transition between versions.

QAInterview questions and model answers

Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.

Intermediate · Conceptual · 1 min · Question 1What can a service worker access that a regular page script cannot, and what can't it access that a page script can?Open model answer

Model answer

It runs on its own thread separate from any page and persists independently of a page's lifetime, letting it intercept network requests and receive push events even when no tab is open. In exchange, it has no access to the DOM and can't directly read or manipulate a page; communication with pages happens through postMessage or the Clients API.

Open question page →
Advanced · Conceptual · 1 min · Question 2Walk through the service worker lifecycle from registration to controlling a page.Open model answer

Model answer

The page calls navigator.serviceWorker.register(), which downloads and evaluates the worker script and fires its install event, typically used to pre-cache assets. Once install succeeds, the worker waits until no page is still controlled by a previous worker, then fires activate, typically used to clean up old caches. Only after activation does it begin controlling pages and intercepting their fetch events — already-open pages stay on the previous worker unless the new one calls clients.claim().

Open question page →
Intermediate · Conceptual · 1 min · Question 3What's the practical difference between cache-first, network-first, and stale-while-revalidate?Open model answer

Model answer

Cache-first checks the cache and only goes to the network on a miss, favoring speed and offline availability over freshness, appropriate for a static app shell. Network-first tries the network first and falls back to cache on failure, favoring freshness while staying usable offline. Stale-while-revalidate returns the cached response immediately for speed, then fetches in the background to update the cache for next time.

Open question page →
Intermediate · Conceptual · 1 min · Question 4Why is deleting old cache versions during the activate event important?Open model answer

Model answer

Each deploy that changes the cache name creates a new, separate cache; if old caches are never deleted, they accumulate indefinitely in the user's storage and can be accidentally matched by a caches.match() call that isn't scoped to the current cache name, serving stale assets alongside current ones.

Open question page →
Advanced · Conceptual · 1 min · Question 5Why doesn't a newly activated service worker automatically take over pages that are already open?Open model answer

Model answer

By default, a page stays controlled by whichever worker was controlling it when it loaded, so a page's already-loaded resources aren't suddenly served by different code mid-session; a new worker only starts controlling existing pages after they're fully closed and reopened, unless it explicitly calls clients.claim(), often paired with skipWaiting() in install, to take over immediately.

Open question page →
Advanced · Conceptual · 1 min · Question 6What's the risk of calling skipWaiting() unconditionally on every deploy?Open model answer

Model answer

It forces the new worker to activate immediately and take over open tabs, which can serve page HTML from one version alongside JS or CSS from a newer cached version if the page doesn't also reload, producing version-mismatch bugs; many apps instead prompt the user to refresh once an update is detected.

Open question page →
Intermediate · Conceptual · 1 min · Question 7How would you keep an offline-capable app's API data reasonably fresh without losing offline access to it?Open model answer

Model answer

Use network-first or stale-while-revalidate for those requests rather than cache-first: network-first tries live data and only falls back to the last cached response when offline, while stale-while-revalidate shows the cached response instantly and refreshes it in the background, so the app is never blocked on the network but isn't stuck on indefinitely stale data.

Open question page →
Advanced · Conceptual · 1 min · Question 8Can a service worker cache a response from a different origin, like a CDN-hosted font or image?Open model answer

Model answer

Yes, but a cross-origin response without CORS is stored as an opaque response — the cache can replay it, but JavaScript can't inspect its status code or body, so a failed request such as a 404 can be cached and replayed as if it succeeded; requesting the resource with CORS mode when possible avoids that blind spot.

Open question page →
Beginner · Conceptual · 1 min · Question 9What's required for a web app to be installable as a PWA, beyond just having a registered service worker?Open model answer

Model answer

A web app manifest describing the app's name, icons, and display mode, served over HTTPS, alongside a service worker that at minimum handles the fetch event — browsers use the combination as the installability signal, though exact criteria vary slightly by browser.

Open question page →
Advanced · Conceptual · 1 min · Question 10How does background sync differ from just retrying a failed fetch in application code?Open model answer

Model answer

A background sync request registered with the service worker is deferred by the browser until connectivity is actually available, and can complete even if the page that registered it has since been closed, whereas a manual retry in page code only runs while that page stays open and the network hasn't dropped mid-retry.

Open question page →

SCScenario questions

Scenario 1

After shipping a bug fix, some users report the site is still broken for them even after a hard refresh, while others see the fix immediately.

  1. Check whether the cache name or version was bumped for this deploy.
  2. Check the fetch handler's strategy for the app shell — cache-first would keep serving old files.
  3. Check whether activate deletes old cache versions.
  4. Decide and implement a deliberate update strategy instead of leaving it to chance.
Reveal worked answer

This points to the service worker serving a stale cached app shell to some users. I'd first check whether the cache name changed with this deploy — if it didn't, cache-first requests keep matching old cached files regardless of what's actually deployed. I'd also check that activate deletes previous cache versions, and that the update path is deliberate: either detect a waiting worker and prompt the user to refresh, or call skipWaiting()/clients.claim() paired with a full reload, so a user never runs old HTML against new assets or vice versa.

Verify and go deeper