Cache-Control a stale-while-revalidate: průvodce pro rok 2026
Praktický průvodce Cache-Control a stale-while-revalidate v roce 2026. CDN vzory, Next.js 15 ISR, hlavička Cache-Status z RFC 9211 a časté chyby s ukázkami kódu.
Cache-Control s direktivou stale-while-revalidate funguje tak, že prohlížeč nebo CDN servíruje zastaralou odpověď okamžitě a na pozadí stahuje čerstvou verzi. Pro běžnou HTML stránku to znamená TTFB pod 50 ms i pro „chladný“ požadavek, protože uživatel nikdy nečeká na origin. V tomhle průvodci ukážu, jak v roce 2026 správně poskládat hlavičky Cache-Control, kdy nasadit stale-while-revalidate, jak se to liší od ISR v Next.js 15 a co umí nová hlavička Cache-Status z RFC 9211.
stale-while-revalidate=N dovolí servírovat odpověď až N sekund po vypršení max-age. Mezitím se na pozadí načte čerstvá verze.
Pro CDN používejte s-maxage samostatně. Oddělíte tak TTL na edge od TTL v prohlížeči, což je klíč k bezpečné invalidaci.
no-cache NEznamená „necachovat“. Znamená „vždy se zeptat na revalidaci přes ETag“. Pro „nikdy necachovat“ slouží no-store.
ISR v Next.js 15 je v podstatě s-maxage=X, stale-while-revalidate řízené z aplikační vrstvy, ne z hlaviček.
Hlavička Cache-Status (RFC 9211) je v roce 2026 podporovaná u Cloudflare, Fastly i Vercel. Konečně je vidět, který hop trefil nebo minul cache.
Imutabilní assety (hashované JS a CSS) chtějí public, max-age=31536000, immutable. Nic jiného. Žádné SWR.
Základy Cache-Control: čistě a od nuly
Hlavička Cache-Control je primární řídicí mechanismus HTTP cache podle RFC 9111. Klient (prohlížeč) a sdílené cache (CDN, reverzní proxy) na ní stojí. Stará dvojice Expires + Pragma je dnes čistě fallback. Pokud nemíříte na zákazníky s prohlížeči staršími než deset let, můžete je ignorovat.
Direktivy se skládají do jedné hlavičky oddělené čárkami. V roce 2026 stojí za to znát zhruba tuhle sadu:
public: smí ukládat i sdílené cache (CDN). Bez téhle direktivy jsou některé proxy konzervativní.
private: smí cachovat jen koncový prohlížeč. Použijte pro odpovědi vázané na uživatele.
max-age=N: čerstvost v sekundách (platí pro všechny cache, pokud není přebito).
s-maxage=N: čerstvost POUZE pro sdílené cache. Přebíjí max-age na CDN.
stale-while-revalidate=N: tolerovat zastaralost N sekund a tiše revalidovat na pozadí.
stale-if-error=N: pokud origin chybuje, držet starou odpověď N sekund místo 5xx.
no-cache: vždy revalidovat (přes ETag nebo If-None-Match) před použitím.
no-store: vůbec neukládat. Pro citlivá data, ne pro „čerstvé“ HTML.
immutable: obsah se nikdy nezmění. Prohlížeč přeskakuje revalidaci i při reloadu.
Mentálně to vidím takhle. max-age a s-maxage kreslí časový „čerstvý“ rámec, stale-while-revalidate kolem něj přidává „tolerovaný“ rámec. Jakákoli žádost uvnitř čerstvého rámce dostane cache bez ptaní. Žádost v toleranci dostane cache okamžitě a spustí revalidaci na pozadí. Mimo oba rámce se musí origin osobně ozvat. Tahle mentální mapa mi zachránila desítky debugovacích hodin, protože většina „tajemných“ cache chyb je v podstatě o nejasné hranici mezi těmihle třemi pásmy.
Jak funguje stale-while-revalidate v praxi
Direktiva stale-while-revalidate přišla z RFC 5861, ale v roce 2026 ji konečně podporují všechny moderní prohlížeče i hlavní CDN. Princip je jednoduchý. Po vypršení max-age nezmizí cachovaná odpověď, ale na nastavený interval se značí jako „stale“. Klient ji v té době dostane bez čekání a cache si paralelně sáhne na origin pro novou verzi.
Konkrétní příklad pro homepage e-shopu, kterou chci servírovat rychle, ale ne s dvouminutovou prodlevou při promo akcích:
Prohlížeč nikdy lokálně necachuje (max-age=0), takže každý request jde minimálně na CDN.
CDN drží odpověď čerstvou 60 sekund. Po 60. sekundě stále servíruje stejnou odpověď z paměti, ale spouští fetch na origin.
Tolerance trvá 600 sekund. Když uživatel zaklikne stránku 65 sekund po posledním invalidačním eventu, dostane HTML za ~5 ms z edge cache. Další uživatel za pár sekund už dostane novou verzi.
Klíčový důsledek: origin nikdy nevidí špičku revalidací. Při 10 000 RPS na cachovanou URL bude origin vidět zhruba 1 request za s-maxage interval. Tomu se říká „request coalescing“ a u Cloudflare i Fastly funguje implicitně, pokud máte SWR a stejný cache key. Honestly, tohle byla jedna z prvních věcí, co mě dostaly do SWR. Když chcete jít hlouběji do toho, co tohle udělá s vašimi origin servery, doporučuju projít si techniky pro snížení TTFB pod 200 ms. SWR je vlastně jedna z nich aplikovaná na úrovni edge.
Jaký je rozdíl mezi max-age a s-maxage?
Tohle je nejčastější otázka, kterou ode mě dostávají backendové týmy migrující na CDN. Krátká odpověď: max-age platí pro všechny cache, s-maxage jen pro sdílené („shared“). Pokud jsou v hlavičce obě, sdílené cache použijí s-maxage, prohlížeč ignoruje sdílenou hodnotu a použije max-age.
V praxi to umožňuje rozdělit cachování na dvě nezávislé vrstvy. Vzor, který používám pro statické HTML s občasnou změnou:
Prohlížeč drží stránku 30 sekund. Krátké, ale stačí to na proklikávání. Sekvenční requesty z téhož uživatele neletí na CDN.
CDN drží stránku až 24 hodin. Když pošlu purge přes API, vrstva se vyčistí okamžitě, takže dlouhé TTL nejsou problém.
Stale tolerance 7 dní pokrývá výpadky originu i edge cold cache po deployi.
Klíčové pravidlo: krátké max-age + dlouhé s-maxage + invalidace přes CDN API. Tohle je vzor, který škáluje. Opačný vzor (dlouhé max-age) je past. Když pustíte do světa stránku s max-age=86400 a najdete v ní typo, čeká vás 24 hodin trapnosti, protože prohlížeče poslechnou hlavičku a budete na ně mít nulovou páku. CDN purge tu nepomůže, protože už nemáte kontrolu nad tím, co je v prohlížečové cache každého uživatele. (Tohle jsem si jednou odbyl naživo na launchi a nikomu to nepřeju.)
Kdy použít no-cache a kdy no-store?
Tady se plete spousta lidí, protože jména jsou matoucí. no-cache NEZNAMENÁ „necachuj“. Znamená „ulož si to, ale před každým použitím se zeptej, jestli je to ještě platné“. Revalidace probíhá přes ETag nebo Last-Modified. Pokud server odpoví 304 Not Modified, klient použije cachovanou verzi.
To je z hlediska výkonu dobrý kompromis pro odpovědi, které se mění zřídka, ale musí být vždy aktuální (dashboardy, uživatelské profily). 304 odpověď je obvykle pod 1 KB, ušetříte přenos, ale ne round-trip latenci. Pro detailní specifikaci doporučuju projít si i MDN dokumentaci k Cache-Control, kde jsou všechny direktivy přehledně popsané včetně edge cases.
no-store naopak říká „vůbec se mě nedotýkej, ani v paměti, ani na disku, ani v service workeru“. Je to ekvivalent „tahle odpověď neexistovala“. Použijte pro:
Stránky s tokeny po jednom použití (CSRF, OTP).
Bankovní detaily, PII, zdravotní záznamy.
Jakoukoli odpověď, kterou by sdílený počítač neměl ukázat dalšímu uživateli.
Pokud chcete jen „tahle HTML stránka má být vždy z čerstvého originu“, použijte Cache-Control: private, max-age=0, must-revalidate. To dovolí prohlížeči ukládat, ale donutí ho revalidovat při každém použití. A hlavně to nezakáže service workeru nebo CDN dělat optimalizace, kde to dává smysl.
Vzory pro CDN: Cloudflare, Fastly, Vercel
Každá CDN respektuje standardní hlavičky, ale má i vlastní rozšíření. V roce 2026 se mi osvědčilo používat standardní hlavičky všude, kde to jde, a vendor-specific direktivy jen pro funkce, které jinak nejdou.
Funkce
Cloudflare
Fastly
Vercel
Respektuje s-maxage
Ano
Ano
Ano
Respektuje stale-while-revalidate
Vyžaduje „Cache Reserve“ nebo Tiered Cache
Ano, nativně
Ano
Vlastní override hlavička
CDN-Cache-Control
Surrogate-Control
CDN-Cache-Control
Purge přes URL
API, max 30 URL na request
Instant Purge, <150 ms globálně
API + tag-based
Cache key zahrnuje query stringy
Konfigurovatelné
Konfigurovatelné
Automaticky vše
Hlavička Cache-Status (RFC 9211)
Ano (od 2024)
Ano
Ano (od 2025)
Doporučený vrstvený přístup, který nasazuji u většiny klientů:
// HTML stránka s aktualizací každou minutu, ale s dlouhou tolerancí
CDN-Cache-Control: public, s-maxage=60, stale-while-revalidate=86400
Cache-Control: public, max-age=0, must-revalidate
Co tohle dělá: CDN čte CDN-Cache-Control a cachuje agresivně. Prohlížeč ji ignoruje (zná jen Cache-Control) a chová se konzervativně, tedy vždy revalidovat. Výsledek? Rychlé edge servírování i okamžitý refresh při změně.
Cache-Control v Next.js 15 a vztah k ISR
Inkrementální statická regenerace (ISR) v Next.js 15 je v jádru jen syntaktický cukr nad stale-while-revalidate na úrovni Vercel CDN. Když napíšete export const revalidate = 60, framework Vercelu řekne „cachuj 60 sekund jako čerstvé, dál tolerantně“, a vygenerované odpovědi dostanou efektivně tyhle hlavičky:
Bez explicitního čísla u SWR. Vercel tam dosadí nekonečno. Stránka tak nikdy nepřestane být dostupná, dokud běží edge cache. Detaily jsou v oficiální dokumentaci Vercel ISR.
U App Routeru v Next.js 15 můžete cachovací chování řídit i jemněji přes fetch() options:
// app/products/[id]/page.tsx
async function getProduct(id: string) {
// Cachovat 5 minut, pak tolerovat starou odpověď do dalšího requestu
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { revalidate: 300, tags: [`product-${id}`] },
});
return res.json();
}
// Invalidace z route handleru nebo server action:
import { revalidateTag } from 'next/cache';
revalidateTag(`product-42`); // edge cache pro produkt 42 zmizí okamžitě
Tagged invalidation je v roce 2026 standardem u všech velkých CDN. Přidává to možnost vyřadit z cache stovky URL jedním API voláním. Pokud spravujete katalogový web, je to často mnohem lepší než časové TTL.
Debug s hlavičkou Cache-Status
Roky jsem řešil debug cache stylem „curl -I a hádej, jestli hit nebo miss“. RFC 9211 přineslo standardní hlavičku Cache-Status, kterou v roce 2026 podporuje Cloudflare, Fastly i Vercel. Vypadá takhle:
tak víte, že Cloudflare cachovaný objekt neměl a šel až k Vercelu. Typicky proto, že cache key se liší (např. díky Vary: Accept-Encoding a jiné kompresi). Pokud chcete jít hlouběji do toho, jak ladit požadavky externích služeb na stránku, podívejte se i na audit a optimalizaci third-party skriptů. Cache-Status často odhalí, že 80 % requestů pochází z analytics a vůbec by se nemělo cachovat.
Praktický debug workflow
Pošlete request s curl -sI https://example.com/page | grep -i cache.
Pokud vidíte miss tam, kde čekáte hit, podívejte se na Vary. Časté překvapení je Vary: Cookie, který fragmentuje cache na uživatele.
Druhý request hned poté. Pokud je už hit, máte funkční cache, jen byl objekt v cold stavu.
Třetí request po vypršení s-maxage. Pokud je hit; fwd=stale, SWR funguje. Pokud je miss; fwd=stale, něco brání revalidaci na pozadí.
Časté chyby, kterých se v roce 2026 zbavte
Cache-Control: no-cache na imutabilních assetech
Hashované soubory typu app.7f3a9b.js jsou z definice imutabilní. Jejich obsah se nikdy nezmění, protože změna obsahu změní hash. Servírujte je s public, max-age=31536000, immutable. Když místo toho dáte no-cache, prohlížeč pošle revalidační request na každý JS soubor při každé navigaci. Na typické SPA aplikaci to znamená 30+ zbytečných round-tripů na obnovení stránky.
Stejné max-age pro HTML a API
HTML a JSON API mají úplně jiný invalidační profil. HTML se mění při deployi nebo CMS update; API odpovědi se mění s každým write. Pro API použijte private, max-age=0, must-revalidate a spolehněte se na ETag. Pro HTML použijte SWR vzor popsaný výše.
Zapomenutý Vary
Pokud servírujete různé HTML pro různé hlavičky (např. Accept-Language, Accept-Encoding, custom auth), MUSÍ tam být Vary. Jinak CDN servíruje cachovanou verzi pro nesprávný kontext, např. anglickou stránku českému uživateli nebo gzip odpověď klientovi, který umí jen brotli.
Mixování Expires a Cache-Control: max-age
Pokud jsou v odpovědi obě, prohlížeč použije max-age a Expires ignoruje. To je fine, ale typicky to znamená, že někdo přidal jednu hlavičku, zapomněl odebrat druhou a teď se diví, proč mu nová hodnota neplatí. Vyhoďte Expires z konfigurací úplně, v roce 2026 nepotřebujete fallback.
Cache pro autorizovaný obsah bez private
Pokud vracíte personalizovanou stránku (např. s uživatelským jménem v hlavičce) a hlavička je public, max-age=60, CDN ji s radostí naservíruje dalšímu uživateli. Pravidlo: vše, co obsahuje data konkrétního uživatele, musí mít private (nebo no-store, pokud je opravdu citlivé).
Často kladené otázky
Co přesně dělá stale-while-revalidate?
Direktiva říká cache (prohlížeči nebo CDN), že po vypršení max-age smí ještě N sekund vracet zastaralou odpověď, pokud současně spustí revalidaci na pozadí. Uživatel tedy nikdy nečeká na origin, dokud je v toleranci.
Podporují stale-while-revalidate všechny prohlížeče?
V roce 2026 ano. Chrome, Edge, Firefox i Safari direktivu respektují pro navigační requesty. Safari má historicky omezenou podporu pro fetch() API requesty, ale pro HTML stránky to funguje všude. CDN podporu má každý velký poskytovatel.
Jaký je rozdíl mezi must-revalidate a no-cache?
no-cache donutí klienta revalidovat pokaždé, kdy by chtěl odpověď použít. must-revalidate donutí klienta revalidovat až POTÉ, co odpověď zestárne (vyprší max-age). V mezičase je must-revalidate tedy povolnější, čerstvá odpověď se servíruje z cache bez ptaní.
Mám použít ETag nebo Last-Modified?
Pokud generujete odpovědi deterministicky, použijte ETag. Hash obsahu je přesnější než časové razítko. Last-Modified má rozlišení 1 sekundu a u rychle se měnícího obsahu může způsobit kolize. Pro statické soubory ze souborového systému je naopak Last-Modified levnější (mtime souboru) a stačí.
Funguje SWR pro POST nebo jen pro GET?
HTTP cache obecně cachuje jen idempotentní metody, tedy GET a HEAD. POST, PUT a DELETE nikdy nejsou cachované, takže stale-while-revalidate u nich nemá smysl. Pokud máte read endpoint implementovaný jako POST (běžné u GraphQL), nebudete moci využít HTTP cache vůbec. Potřebujete aplikační cache vrstvu.
Nativní API v Chrome pro deklarativní prerender stránek a navigaci pod 100 ms. Návod: syntaxe, eagerness úrovně, omezení Cache-Control, ladění v DevTools.
Praktický návod, jak změřit a snížit dopad third-party skriptů na INP a LCP. DevTools audit, fasády, Partytown ve Web Workeru, performance budget a CI checks pro rok 2026.
Praktický průvodce optimalizací obrázků v roce 2026: AVIF vs WebP, responzivní srcset, fetchpriority pro LCP, lazy loading a srovnání CDN. S ukázkami kódu z produkce.