A courier app needs its own pantry for network responses - a place the worker fully controls and whose contents survive a reload and a dropped connection. That pantry is Cache Storage, also known as the Cache API. The temptation to treat it as just another layer of the browser cache is strong, but that very misconception leads to the most tangled freshness bugs.
By design it is a store of request-response pairs managed from JavaScript. The caches.open(name) call opens - or creates - a named container. A container has four basic operations: match looks up a matching Response by Request, put saves a request-response pair, delete removes an entry, keys lists the requests stored in it. There is no automation here: an entry appears and disappears only when your code explicitly asks for it.
Hence the main difference from the browser's HTTP cache. The HTTP cache obeys Cache-Control headers and works under the hood on its own. Cache Storage does not inherit your freshness policy - it holds exactly what you put into it, and until you delete it. A Cache-Control: no-store header arriving later will not, by itself, erase an entry you explicitly saved. What is more, the HTTP cache still exists below: when the worker goes to the network via fetch(), the response may already come from it. The two caches live at different levels, and confusing them means losing control over which version the user sees.
The second source of surprises is the cache key. A match is found by URL and method, and a Vary header in the response can narrow it down to specific request headers. The problem lies in authorized data: two GETs to the same URL - an orders list - return different content depending on the cookie or token, even though the address matches. Drop such a response into a shared cache with no tie to the user, and you hand the next person to sign in someone else's orders. So personal responses are either not cached at all, or placed in a namespace bound to the account and purged on sign-out.
On this foundation rests the simplest strategy - Cache First. Its logic is direct: look in the cache first, and if the response is there, return it immediately without touching the network; only on a miss go to the network and save the response for next time. The ideal candidate is an immutable asset with a content hash in its name, for example app.83fd1.js. Because the content is tied to the name, staleness cannot occur: change the content and the URL changes with it.
The benefit is obvious: minimal latency, offline operation, saved bandwidth - the response is served from local storage in milliseconds. But the strategy has a cost, and it is paid in freshness. Without URL versioning, without an age limit, and without eviction, the user may see the same old asset for as long as you like - the cache does not expire on its own. The worst candidate for Cache First is mutable HTML with no lifetime: cache it once and you risk locking the person into an old version of the app forever.
Hence a set of caveats. For catalog images, which also fit Cache First well, you add a limit on the count and age of entries, or storage will grow uncontrolled. For hash-built bundles it is more reliable not to rely on random runtime fill but to place them in the cache ahead of time, during install - then the shell is guaranteed to be in place for the first offline moment. Cache First is a choice in favor of speed where the content is immutable or its staleness does no harm; otherwise you need a strategy that remembers the network.
async function safePut(cache, request, response) {
if (!response || !response.ok) return;
if (response.type === 'opaque') return; // if there is no separate policy
const cc = response.headers.get('cache-control') ?? '';
if (/\bno-store\b/i.test(cc)) return;
await cache.put(request, response.clone());
}async function cacheFirst(request, cacheName) {
const cache = await caches.open(cacheName);
const hit = await cache.match(request);
if (hit) return hit;
const response = await fetch(request);
if (response.ok) await cache.put(request, response.clone());
return response;
}