Téměř každý web potřebuje stejná dvě pravidla. Soubory s hashem obsahu v názvu - app.9f2c1b.js - dostanou Cache-Control: public, max-age=31536000, immutable, protože se název změní spolu s obsahem a prohlížeč se už nikdy nemusí ptát. HTML dostane Cache-Control: no-cache, což stránku uloží, ale před použitím ji znovu ověří, takže deploy je vidět okamžitě a opakovaná návštěva stojí jen jedno levné 304 místo celého stažení.
To je devadesát procent cachování webu. Zbytek tohoto článku je těch zbylých deset procent: co která direktiva doopravdy znamená, proč jsou tvé hlavičky ignorovány a co dělá CDN jinak než prohlížeč.
Cache mezi tvým serverem a návštěvníkem#
Odpověď se dá uložit na víc místech, než si většina lidí představuje, a každé se řídí trochu jinými pravidly.
Podstatný je rozdíl mezi private a shared. Cache prohlížeče patří jednomu člověku, takže smí držet stránku jeho účtu. CDN je sdílená všemi, takže nesmí - a direktiva private je způsob, jak to říct. Splést si to znamená, že potvrzení objednávky jednoho zákazníka dostane jiný, což je špatný den, který začíná hlavičkou.
Tvá reverzní proxy stojí mezi nimi a normálně necachuje vůbec nic, dokud ji k tomu nenastavíš. Na RE:NODE slot proxy ukončuje TLS a přeposílá na tvůj server; rozhodnutí o cachování zůstávají na tvé aplikaci a na CDN, kterou před ni případně dáš.
Cache-Control, direktiva po direktivě#
Cache-Control je hlavička odpovědi s čárkami odděleným seznamem. Tyto direktivy si své místo zaslouží:
| Direktiva | Platí pro | Co znamená |
|---|---|---|
max-age=600 | Všechny cache | Čerstvá 600 sekund od teď |
s-maxage=600 | Jen sdílené cache | Přebíjí max-age pro CDN a proxy |
public | Všechny cache | Smí se uložit, i když byl požadavek ověřený |
private | Jen prohlížeč | Sdílená cache ji nesmí uložit |
no-cache | Všechny cache | Ulož ji, ale před každým použitím ověř |
no-store | Všechny cache | Vůbec si ji nezapisuj |
must-revalidate | Všechny cache | Jakmile zastará, nikdy ji neservíruj bez ověření |
immutable | Prohlížeč | Neověřuj, ani při reloadu |
stale-while-revalidate=60 | Všechny cache | Servíruj zastaralou 60 s, zatímco se stahuje čerstvá |
stale-if-error=86400 | Všechny cache | Servíruj zastaralou, když origin selhává |
Dvě z nich se pletou neustále. `no-cache` neznamená necachuj. Znamená ulož a vždy se nejdřív zeptej, což je přesně to, co chceš pro HTML: cesta tam a zpět je titěrná, když se nic nezměnilo. no-store je ta, která znamená nedrž kopii, a patří k odpovědím, které se nesmí nikdy dotknout disku - výpis z banky, jednorázový token, stránka pro reset hesla.
immutable je direktiva, díky které jsou hashované soubory při opakované návštěvě opravdu zadarmo. Bez ní prohlížeč při reloadu znovu ověřuje každý uložený soubor a ty platíš cestu tam a zpět za každý asset, jen aby ti bylo řečeno, že se nic nezměnilo. S ní prohlížeč kontrolu po dobu max-age úplně přeskočí. Je bezpečná jen na URL, jejíž obsah se nikdy nezmění, což je přesně to, co hash obsahu zaručuje.
stale-while-revalidate je nejlevnější výkonnostní zisk, který většina webů nikdy nezapne. max-age=60, stale-while-revalidate=600 znamená, že uložená stránka se servíruje okamžitě v nejhorším jedenáct minut a obnovení probíhá na pozadí, ne před očima návštěvníka.
Pokud neposíláš žádné cachovací hlavičky vůbec, cache smějí hádat. RFC 9111 dovoluje heuristickou čerstvost založenou na Last-Modified - běžně deset procent doby od poslední změny dokumentu - takže soubor změněný před rokem může něco na cestě držet v cache týdny. Ticho neznamená „necachuj“.
ETag, Last-Modified a 304#
Čerstvost říká, jak dlouho smí cache odpověď znovu použít bez ptaní. Validace je to, co se stane, když se ptá.
Last-Modified: Wed, 10 Sep 2026 09:41:12 GMT- časové razítko s přesností na sekundu.ETag: "6f9a2c-1b4d"- neprůhledný token, který se změní, když se změní obsah.W/"..."označuje slabý tag, tedy sémanticky ekvivalentní, ne bajt po bajtu shodný.
Při dalším požadavku prohlížeč pošle If-Modified-Since nebo If-None-Match s hodnotou, kterou drží. Pokud se nic nezměnilo, server odpoví 304 Not Modified s hlavičkami a bez těla a prohlížeč použije kopii, kterou už má.
$ curl -sI https://example.com/style.css | grep -i -E "etag|cache-control|last-modified"$ curl -sI -H 'If-None-Match: "6f9a2c-1b4d"' https://example.com/style.css | head -1HTTP/2 304304 ušetří bajty, ale ne cestu tam a zpět. Při spojení s latencí 120 ms stojí čtyřicet assetů, které všechny vrátí 304, pořád stránku, která působí pomalu. To je celý argument pro dlouhý max-age s immutable na souborech s otiskem: nejrychlejší požadavek je ten, který se nikdy nestane.
nginx generuje ETag pro statické soubory automaticky z času změny a velikosti, což je stabilní napříč servery, pokud mají soubory stejná časová razítka. Deploy, který přepíše každý soubor s novým mtime, zneplatní každý ETag na webu i tam, kde je obsah stejný, takže tam, kde na tom záleží, použij způsob kopírování, který zachovává časová razítka. Aplikační frameworky obvykle hashují tělo odpovědi, což je správně, ale stojí to CPU při každém požadavku.
Dvouúrovňová strategie#
Téměř každý web by měl skončit tady:
| Co | Cache-Control | Proč |
|---|---|---|
| HTML stránky | public, no-cache | Vždy čerstvé, levné 304 při opakování |
| CSS, JS, fonty s hashem | public, max-age=31536000, immutable | Název se změní spolu s obsahem |
| CSS a JS bez hashe | public, max-age=3600 | Nelze dokázat, že se nezměnily |
| Nahrané obrázky | public, max-age=2592000 | Zřídka se přepisují na místě |
| Stránky pro přihlášené | private, no-store | Nikdy ve sdílené cache |
| Odpovědi API | private, max-age=0 nebo no-store | Rozhoduj pro každý endpoint zvlášť, nehádej |
Rok - 31536000 sekund - je konvenční maximum pro max-age a větší číslo nic nepřinese. Pokud tvůj build už zapisuje názvy s hashem, jako každý moderní bundler, horní dva řádky pokrývají celý web; hosting statických webů ukazuje bloky nginxu v kontextu.
Úroveň, která dělá potíže, je třetí: stylesheet na pevné URL, který upravuješ na místě. Pro něj neexistuje bezpečná dlouhá cache, protože někteří návštěvníci budou držet starou kopii tak dlouho, jak jsi jim řekl. Řešením není kratší cache, ale jiná URL. Každý build nástroj umí výstup otiskovat; pokud píšeš HTML ručně, připoj verzi v query - style.css?v=7 - a změň ji, když soubor upravíš. Query string je pro prohlížeče i většinu CDN součástí klíče cache, i když některé CDN lze nastavit tak, aby ho ignorovaly, čímž se z tvého cache bustera stane prázdná operace.
Vary, cookies a hlavičky, které cachování vypínají#
Vary říká cache, které hlavičky požadavku mění odpověď. Je stejnou měrou nutná i nebezpečná, protože každá hodnota znásobuje počet kopií, které musí cache držet.
Vary: Accept-Encoding- správně a nutné, když komprimuješ. Bez ní může cache předat gzipované tělo klientovi, který o ně nežádal.Vary: Cookie- technicky správně na stránce, která se liší podle uživatele, a prakticky to znamená, že sdílená cache nikdy nezíská zásah, protože každý návštěvník má jiný cookie.Vary: User-Agent- roztříští cache na tisíce variant. Vyhni se tomu.
Cookies jsou druhý tichý zabiják cache. Odpověď nesoucí Set-Cookie nemůže sdílená cache uložit pro použití jinými lidmi a většina CDN při jeho spatření cache prostě obchází. To je správné výchozí chování, a proto analytický nebo consent skript, který na každé stránce nastavuje cookie na straně serveru, může poloviční cache hit rate snížit, aniž by si toho kdo všiml.
PHP to dělá samo od sebe. První volání session_start() pošle pro celou odpověď:
Expires: Thu, 19 Nov 1981 08:52:00 GMTCache-Control: no-store, no-cache, must-revalidatePragma: no-cacheTo dělá svou práci session.cache_limiter a je to správně na stránce, která ukazuje něčí účet. Je to špatně na veřejné stránce, která náhodou bez důvodu zahájí session. Buď session na stránkách, které ji nepotřebují, nespouštěj, nebo nastav session_cache_limiter('public') před session_start() a přebírej odpovědnost za to, co děláš.
WordPress a většina cache stránek v CMS se řídí stejnou logikou: cokoli s přihlašovacím cookie obchází cache úplně, a proto admin, který web testuje, vidí pomalou cestu a hlásí, že cachování nefunguje. Testuj v anonymním okně. Rychlost a cachování WordPressu prochází vrstvy v tomto konkrétním případě.
CDN a sdílené cache#
CDN je sdílená cache na mnoha místech. Čte stejné hlavičky se dvěma doplňky, které stojí za to znát.
s-maxage platí jen pro sdílené cache, což ti umožní držet HTML čerstvé v prohlížečích a zároveň v cache na okraji: Cache-Control: public, max-age=0, s-maxage=600, stale-while-revalidate=3600. Prohlížeče ověřují pokaždé, CDN servíruje desět minut z vlastní kopie a obnovuje na pozadí a tvůj origin vidí zlomek provozu. Pro web, jehož stránky jsou pro všechny stejné, je to nejvýznamnější jediná změna, kterou můžeš udělat.
CDN-Cache-Control je cílená hlavička, kterou čtou jen CDN, takže jim můžeš dát jiné pokyny, aniž bys sáhl na to, co vidí prohlížeče. Starší infrastruktura používá ke stejnému účelu Surrogate-Control. Podpora se liší, takže si před spoléháním ověř dokumentaci svého poskytovatele - Cloudflare pro weby a herní servery popisuje, co s nimi dělá ta nejběžnější.
S CDN souvisejí dva provozní návyky. Za prvé, vědět, jak čistit cache, podle URL i podle tagu, a dát purge do deploy skriptu místo do runbooku, který nikdo neotevře. Za druhé, číst hlavičku se stavem cache, kterou poskytovatel posílá - HIT, MISS, EXPIRED, BYPASS - protože z „funguje cachování?“ udělá fakt místo hádky. Pokud vidíš BYPASS, hledej cookie.
CDN nespraví pomalý origin u necachovaných požadavků. První návštěvník každé stránky na každém místě stále čeká na tvůj server, stejně jako každý požadavek, který cookie udělá necachovatelným. TTFB, Core Web Vitals a hosting popisuje, kterou část čekání můžeš opravit ty.
Nastavení hlaviček#
V nginxu zvlášť statické assety podle přípony a zvlášť HTML:
location ~* \.(?:css|js|woff2|avif|webp|png|jpg|svg)$ { expires 1y; add_header Cache-Control "public, max-age=31536000, immutable" always; access_log off;}location ~* \.html$ { add_header Cache-Control "public, no-cache" always;}expires 1y; nastaví jak starší hlavičku Expires, tak odpovídající max-age, a proto po něm následuje explicitní add_header: chceš ty další direktivy.
Past nginxu, která dostane každého: direktivy add_header se dědí z nadřazeného bloku jen tehdy, pokud aktuální blok nedefinuje žádné vlastní. Přidej jediný add_header do location a každá hlavička nastavená v nadřazeném bloku server z těch odpovědí zmizí - včetně bezpečnostních hlaviček. Pokud používáš add_header v location, zopakuj všechno, co nastavil rodič. Příznak always způsobí, že se hlavička použije i u chybových odpovědí, nejen u 2xx a 3xx.
Z PHP ji nastav před jakýmkoli výstupem a pamatuj, že nastavení bufferování výstupu rozhoduje, jak brzy „před jakýmkoli výstupem“ doopravdy je - nastavení php.ini, na kterých záleží popisuje ta, která kousají:
header('Cache-Control: public, max-age=300, stale-while-revalidate=600');Z aplikačního frameworku používej jeho pomocníky pro odpovědi místo holých hlaviček, protože framework může později přidat vlastní a vyhrává ta zapsaná poslední.
Kontrola toho, co skutečně posíláš#
Nikdy nevěř konfiguraci; čti, co jde po drátě. Jeden příkaz ti řekne většinu:
$ curl -sI https://example.com/ | grep -i -E "cache-control|etag|age|vary|set-cookie"Na co se dívat, v tomto pořadí:
- `Set-Cookie` na veřejné stránce. Skoro vždy nehoda a vypíná sdílené cachování.
- `Age`. Jeho přítomnost znamená, že uloženou kopii servíroval někdo před tebou, a hodnota udává stáří té kopie.
- `Vary`. Cokoli nad rámec
Accept-Encodingpotřebuje důvod. - Protichůdné direktivy.
no-cachespolečně smax-age=3600je legální a matoucí;no-storepřebíjí všechno. - Panel Network ve vývojářských nástrojích prohlížeče. Sloupec velikosti ukazující „disk cache“ nebo „memory cache“ bez síťového času je tak, jak vypadá fungující cache. Pamatuj, že tvrdý reload cache obchází a o skutečném zážitku nic neříká.
Když se změna hlaviček tváří, že nic nedělá, zkontroluj v tomto pořadí: servíruje před tebou CDN starou kopii, drží tvůj prohlížeč kopii z doby před změnou a přepisuje tě níže v konfiguraci nějaký blok location. V zhruba osmdesáti procentech případů je to jedna z těchto tří věcí.
FAQ#
Jaký je rozdíl mezi no-cache a no-store?
no-cache odpověď uloží a před každým použitím ji ověří, takže pořád dostaneš 304 a rychlou stránku. no-store zakazuje ji kamkoli zapsat. Používej no-cache pro HTML, které se mění, a no-store jen pro skutečně citlivé odpovědi - vypíná také back/forward cache prohlížeče, takže navigace působí pomaleji.
Mám použít ETag, nebo Last-Modified?
Obojí, pokud jsou zadarmo. Last-Modified má přesnost na sekundu a pro soubory stačí; ETag je přesný a funguje i pro generované odpovědi. Servírování obou nechá klienta použít, co mu vyhovuje. ETagy nevypínej, pokud nemáš konkrétní problém s více servery, který jsi změřil.
Proč je moje CSS po deployi pořád staré?
Protože jsi prohlížečům řekl, ať ho drží. Pokud se URL nezměnila a poslal jsi dlouhý max-age, jediným lékem je čas nebo nová URL. Otiskuj své assety, aby se to už nemohlo stát, a dlouhou cache nech pro soubory, jejichž názvy kódují jejich obsah.
Pomáhá cachování webu s velmi malým provozem?
Pomáhá opakovaným návštěvníkům a pomáhá každému načtení stránky po tom prvním, což je většina toho, co návštěvník zažije. Nepomáhá u prvního požadavku na studenou cache. U malého webu je většinou větší výhra rychlý origin samotný plus komprese.
Jak dlouho cachovat obrázky?
Třicet dní je rozumné výchozí nastavení pro nahraná média, rok, pokud název souboru obsahuje hash. Selhání nastává, když obrázek přepíšeš na místě a ten starý přetrvá, proto raději nahraj nový soubor s novým názvem, než přepisovat.
Ovlivňují tyto hlavičky pozice ve vyhledávání?
Ne přímo. Ovlivňují, jak rychle se stránky načítají skutečným návštěvníkům, což se měří v terénu a přes Core Web Vitals to vstupuje do signálů pro řazení. Cachování se vyplatí dělat kvůli uživatelům; pořadí je vedlejší efekt.




Komentáře
Zcela anonymně: žádný účet, žádný e-mail, žádné cookie. Ukládáme jen jméno, které napíšeš, text a čas - nic dalšího. Počet odkazů je omezený a značkování se nevykresluje.