Van az a hibajegy, amelyben a szerver teljesen helyesen küld Cache-Control: no-store fejlécet, a felhasználó mégis a tegnapi, vagy rosszabb esetben az előző bejelentkezett felhasználó adatait látja. Ilyenkor könnyű a CDN-re mutogatni. Néha megérdemli, de gyakran nem ő a tettes: a Service Workerünk saját kezűleg eltette a választ a Cache API-ba.

A név megtévesztő. A Cache API nem a böngésző HTTP-cache-ének programozható távirányítója, hanem az originhez tartozó, névvel ellátott Request/Response tárolók halmaza. A böngésző csak akkor ad innen választ, ha te – tipikusan egy fetch eseménykezelőben – visszaadod neki. Ez inkább egy kicsi, lemezen tárolt adatbázis HTTP-objektumokhoz, nem pedig egy okos proxy.
Két cache, két felelős
A HTTP-cache a Cache-Control, az ETag és a többi HTTP-szabály alapján dönt. Frissnek számít-e a válasz, újra kell-e érvényesíteni, eltárolható-e egyáltalán? Ezt a böngésző intézi a hálózati kérés körül.
A Cache API-nál a döntés a te kódodé. Egy bejegyzés nem frissül magától, és nem jár le magától: addig marad, amíg delete()-tel, cache-verzióváltással vagy teljes törléssel ki nem veszed. A böngésző kvótanyomás alatt eldobhatja az origin teljes tárolóját, tehát ez nem archiválási rendszer; de a lejárat hiánya így is pontosan elég egy hónapokig élő hibához.
A kevésbé ismert részlet: a Cache.put() specifikációja a nem GET kérés, a 206-os részválasz és a Vary: * esetét utasítja el. A Cache-Control: no-store nincs ezen a listán. Ha a saját kódod meghívja a put()-ot, a szabványos HTTP-cache tiltása nem ment meg. Na, ezt nem tudtam, amíg egyszer nem láttam egy „ne cache-eld” API-választ hetekkel később is offline módban visszajönni.
A cache-first minta nem API-cache stratégia
Az alábbi, szinte minden PWA-példában felbukkanó gondolat csak immutable asseteknél biztonságos: „ha van találat, add vissza, különben töltsd le és tedd el”. Egy /assets/app.8c31.js fájlnál ez remek. Egy /api/me, keresési találat vagy kosárválasz esetében csendes adatfrissességi és könnyen adatvédelmi hiba.
Én a gyorsítótárba írás elé külön, szándékosan szűk politikát tennék. A példa ugyanazt a függvényt futtatja Service Workerben és tesztben; a no-store és a private itt nem böngészőparancs, hanem a saját tárolónk számára értelmezett szerződés.
function shouldStore(request, response, appOrigin) {
const cacheControl = response.headers.get('Cache-Control') ?? '';
return request.method === 'GET'
&& new URL(request.url).origin === appOrigin
&& response.ok
&& !/\b(?:no-store|private)\b/i.test(cacheControl);
}
self.addEventListener('fetch', (event) => {
const task = (async () => {
const cache = await caches.open('assets-v4');
const hit = await cache.match(event.request);
if (hit) return { response: hit };
const response = await fetch(event.request);
const copy = shouldStore(event.request, response, self.location.origin)
? response.clone()
: null;
return { response, cache, copy };
})();
event.respondWith(task.then(({ response }) => response));
event.waitUntil(task.then(({ cache, copy }) =>
copy ? cache.put(event.request, copy) : undefined));
});
A clone() nem dísz. A put() elfogyasztja a response törzsét, ezért ugyanazt a példányt nem adhatjuk vissza a lapnak. A waitUntil()-t ráadásul még az esemény callbackjében hívjuk meg, a cache-írás ígérete pedig később teljesül. Így az írás a Service Worker élettartamához kötődik; enélkül egy rövid életű workernek esélye van a tárolás vége előtt eltűnni. Az MDN leírása erre a látszólag fölösleges részletre adja a jó indokot.
A Vary itt sem csak egy HTTP-dísz
A Cache API találata alapból figyelembe veszi a tárolt válasz Vary fejlécét, tehát nem pusztán az URL-t nézi. A match() dokumentációja szerint ezt az ignoreVary: true opcióval ki is lehet kapcsolni. Productionben ez szinte mindig rossz ötlet: egy nyelv, tartalomtípus vagy autentikáció szerint eltérő válasznál te magad kapcsolod ki az egyetlen beépített elválasztót. A Vary segít, de nem helyettesíti azt a döntést, hogy személyes API-választ eleve nem teszünk ide.
Verziózz, takaríts, mérj
A jól használható recept kevésbé látványos, mint egy mindenre ráengedett offline réteg: külön cache az immutable build asseteknek; explicit cache-név-verzió; az activate eseményben a régi verzió törlése; dinamikus adatoknál inkább network-first vagy rövid, saját időbélyeges TTL. A Cache API nem olvassa ki neked a max-age-et, tehát ha TTL kell, azt is neked kell tárolnod és ellenőrizned.
Érdemes időnként megnézni a navigator.storage.estimate() eredményét is. Nem precíz számlakivonat, de jelzi, hogy az origin tárolója mennyire hízott el. A böngésző kvóta miatti takarítása nem invalidációs stratégia; olyan, mintha a kukásautótól várnánk a migrációk futtatását.
Productionben használnám, de csak letölthető alkalmazásburokra, hash-elt statikus assetekre és tudatos offline funkciókra. Személyes vagy gyorsan változó API-adatnál az alapértelmezett válaszom továbbra is: ne tedd bele. A Cache API ereje pont az, hogy megkerülheted a HTTP-cache szabályait. Ez egyben az oka annak is, hogy nem szabad véletlenül megkerülni őket.