HTTP caching: czemu druga wizyta na stronie jest natychmiastowa

Weź jeden plik style.css i prześledź jego dzień na działającej stronie: najpierw pobiera go przeglądarka, potem serwuje go z pamięci bez pytania serwera, później pyta serwer, czy jej kopia jest nadal aktualna, i zwykle dostaje samo potwierdzenie zamiast całego pliku, a po wdrożeniu nowej wersji pobiera go od nowa. Z tej jednej historii wychodzi cały HTTP caching - Cache-Control, ETag, 304 i stale-while-revalidate.
Wchodzisz na stronę, wracasz na nią chwilę później - i ładuje się natychmiast. Wdrażasz poprawkę CSS, odświeżasz, a i tak widzisz stary wygląd. Za obydwoma stoi cache: zapisana wcześniej odpowiedź serwera, której przeglądarka użyła ponownie, zamiast pobierać ją od nowa.
Najłatwiej zobaczyć, jak to działa, na jednym pliku. Weźmy style.css i prześledźmy, co się z nim dzieje od pierwszego wejścia na stronę aż po wdrożenie nowej wersji.
#Pierwsze wejście: cache jest pusty
Ktoś otwiera stronę po raz pierwszy. Przeglądarka potrzebuje style.css, ale nie ma go u siebie, więc pobiera go z serwera (z originu - to tam plik naprawdę żyje). W odpowiedzi serwer dokłada kilka nagłówków, które mówią, jak traktować ten plik na przyszłość:
HTTP/1.1 200 OK
Cache-Control: max-age=3600
ETag: "v1-abc"Cache-Control: max-age=3600 znaczy: tę odpowiedź można uznać za świeżą przez 3600 sekund, czyli godzinę. ETag: "v1-abc" to identyfikator tej konkretnej wersji pliku - przyda się później, żeby sprawdzić, czy plik się zmienił.
Przeglądarka zapisuje plik razem z tymi nagłówkami i wyświetla stronę. Pierwsze wejście zawsze kosztuje pełne pobranie - cache nie miał jeszcze czego użyć.
#Kolejne wejście: odpowiedź prosto z cache
Użytkownik przechodzi na inną podstronę, która znów używa style.css. Tym razem przeglądarka nie wysyła żadnego żądania. Widzi, że plik dostała kilka minut temu, a świeżość wynosi godzinę, więc po prostu używa swojej kopii.
To jest ta natychmiastowa druga wizyta: dopóki plik jest świeży, nie ma żadnego ruchu w sieci.
Ta sama kopia może istnieć jeszcze przed przeglądarką. Między nią a originem często stoi CDN - cache współdzielony, który trzyma jedną kopię dla wielu użytkowników. Serwer może dać mu inny czas życia niż przeglądarce:
Cache-Control: max-age=60, s-maxage=3600s-maxage dotyczy tylko cache współdzielonego. Tutaj przeglądarka trzyma plik minutę, a CDN godzinę. Dzięki temu większość użytkowników dostaje plik z najbliższego węzła CDN, bez obciążania originu.
#Po godzinie: plik jest przeterminowany
Po godzinie kopia w przeglądarce staje się przeterminowana (stale). To nie znaczy “do wyrzucenia” - znaczy tylko “nie używaj jej, zanim nie sprawdzisz, czy jest aktualna”.
I to jest najważniejsza oszczędność w całym cachingu. Przeglądarka nie pobiera pliku od nowa - najpierw pyta serwer, czy jej kopia jest wciąż aktualna. Wysyła żądanie warunkowe z identyfikatorem, który dostała wcześniej:
GET /style.css HTTP/1.1
If-None-Match: "v1-abc"Serwer porównuje "v1-abc" z aktualną wersją i odpowiada na jeden z dwóch sposobów:
nic się nie zmieniło → 304 Not Modified (bez body)
jest nowa wersja → 200 OK (pełny, nowy plik)Jeśli plik się nie zmienił, serwer zwraca 304 Not Modified - odpowiedź bez body, samo potwierdzenie “twoja kopia jest dobra”. Przeglądarka dostaje kilka bajtów zamiast całego pliku i dalej używa tego, co ma. Pełne pobranie następuje tylko wtedy, gdy faktycznie pojawiła się nowa wersja.
Dlatego przeterminowanie nie jest problemem - w najgorszym razie kosztuje jedno lekkie sprawdzenie. Zamiast ETag serwer może porównywać daty parą Last-Modified / If-Modified-Since; działa tak samo, a ETag jest tylko dokładniejszy, bo reaguje na każdą zmianę treści, nie tylko na zmianę daty.
Prześledź to na poniższym symulatorze. To style.css z max-age=60:
Klikaj kolejno: wejdź na stronę, przesuń czas, wdróż nową wersję - i patrz, co za każdym razem leci po sieci.
Kliknij „Wejdź na stronę”, żeby zacząć.
#Wdrażasz nową wersję - i widzisz starą
Podmieniasz style.css na nowy, z poprawionym wyglądem, wchodzisz sprawdzić - a tu dalej stara wersja. To nie błąd przeglądarki. Twoja kopia style.css ma jeszcze świeży max-age, więc przeglądarka nie ma po co pytać serwera - robi to, co jej wcześniej kazano: używa świeżej kopii.
Pokusa jest taka, żeby skrócić max-age do zera i mieć spokój. Ale wtedy tracisz cały zysk z cache - każde wejście znów odpytuje serwer. Chcesz dwóch rzeczy, które ciągną w przeciwne strony: trzymać pliki w cache jak najdłużej, a zarazem widzieć wdrożenie natychmiast.
Rozwiązuje to trik, który stosuje praktycznie każdy bundler (Vite, webpack i inne): nazwa pliku zawiera hash jego treści.
style.9f2a3c.cssZmienisz choćby jeden znak w CSS - hash się przelicza i plik nazywa się inaczej (style.7b1e0d.css). Dla cache inna nazwa to inny plik: nikt go jeszcze nie ma, więc przeglądarka pobiera go normalnie. Stara nazwa zostaje w cache, ale nikt już o nią nie poprosi.
Skoro nazwa zmienia się przy każdej zmianie treści, plik pod daną nazwą jest niezmienny - można go cache’ować najdłużej, jak się da:
pliki z hashem (CSS, JS, obrazki) → Cache-Control: max-age=31536000, immutable
HTML, który na nie wskazuje → Cache-Control: no-cachemax-age=31536000 to rok, a immutable dokłada: “nie sprawdzaj nawet po wygaśnięciu, ta nazwa zawsze znaczy ten sam plik”. Przez cały ten czas zero żądań do serwera - i to bezpieczne, bo gdyby treść się zmieniła, plik miałby już inną nazwę.
Zostaje jedno pytanie: skąd przeglądarka wie, że ma teraz wziąć style.7b1e0d.css, a nie stary style.9f2a3c.css? Z HTML-a - to on wypisuje nazwy plików do załadowania. Dlatego HTML jest jedynym plikiem, który musi być świeży od razu po wdrożeniu, i dostaje no-cache: “zapisz, ale zawsze najpierw zapytaj serwer”. To sprawdzenie jest tanie i zwykle kończy się 304.
I to spina całość. Przy każdym wejściu przeglądarka sprawdza tylko HTML. Nie zmienił się - 304, a wszystkie pliki z hashem lecą z cache bez ruchu w sieci. Zmienił się - przychodzi świeży i wskazuje nowe nazwy, których nikt jeszcze nie ma, więc przeglądarka dociąga dokładnie te pliki, które faktycznie się zmieniły. Maksymalny cache i natychmiastowe wdrożenia naraz.
#Nie wszystko można cache’ować jak zwykły plik
Dotąd style.css był wygodny: ten sam plik dla wszystkich, można go trzymać długo. Ale nie każda odpowiedź serwera tak wygląda - strona z danymi zalogowanego użytkownika albo saldo konta nie mogą leżeć w cache jak publiczny plik CSS.
Decyduje o tym ten sam nagłówek Cache-Control, w którym wcześniej siedział max-age. Poza czasem życia może nieść dyrektywy mówiące, kto może zapisać odpowiedź i czy w ogóle. Trzy z nich brzmią podobnie, a znaczą co innego (klasyczne pytanie na rozmowie):
private- odpowiedź jest dla jednego konkretnego użytkownika (np. strona z jego danymi po zalogowaniu). Wolno ją trzymać tylko w cache przeglądarki, nigdy we współdzielonym CDN - inaczej jeden użytkownik mógłby dostać dane drugiego.no-cache- zapisz, ale nigdy nie używaj bez sprawdzenia na serwerze. Brzmi jak “nie cache’uj”, a znaczy “zawsze najpierw zapytaj”. To właśnie tryb HTML z poprzedniej sekcji.no-store- nie zapisuj nigdzie. Dla danych wrażliwych (saldo konta, dane osobowe), które nie mają prawa trafić do żadnego cache.
#Oddaj stare, sprawdź w tle - stale-while-revalidate
Wróćmy do momentu, gdy plik był przeterminowany i przeglądarka musiała poczekać na 304. To krótka chwila, ale przy wolnym serwerze użytkownik i tak czeka. Da się to obejść - opisuje to osobna specyfikacja, RFC 5861:
Cache-Control: max-age=600, stale-while-revalidate=86400Przez 600 sekund plik jest świeży. Po wygaśnięciu, przez kolejne 24 godziny, przeglądarka od razu oddaje przeterminowaną kopię (użytkownik nie czeka), a sprawdzenie aktualności wykonuje w tle. Następny użytkownik dostanie już odświeżoną wersję. Koszt: ktoś czasem zobaczy treść sprzed chwili. Dla większości stron to dobry kompromis.
To ta sama idea, co ISR z postu o strategiach renderowania: oddaj gotową wersję teraz, odśwież w tle. ISR to stale-while-revalidate zastosowany do całych stron HTML, a nie do pojedynczego pliku.
Pokrewna dyrektywa stale-if-error mówi: jeśli serwer padnie (500, 502, 503, 504), oddaj przeterminowaną kopię zamiast błędu. Strona działa dalej na ostatniej dobrej wersji, mimo że backend leży.
#Co zapamiętać
Cały ten przykład sprowadzał się do jednego pytania, które przeglądarka zadaje sobie za każdym razem, gdy potrzebuje pliku: czy w ogóle muszę odzywać się do serwera, a jeśli tak - czy mogę chociaż nie pobierać całości na nowo. Stąd trzy wnioski:
max-ageto świeżość. Dopóki odpowiedź jest świeża, idzie z cache bez ruchu w sieci. Po wygaśnięciu staje się przeterminowana - czyli “sprawdź przed użyciem”, nie “wyrzuć”.- Rewalidacja oszczędza transfer. Przeglądarka pyta
ETag-iem, a serwer odpowiada304 Not Modifiedbez body, jeśli nic się nie zmieniło. Pełne pobranie tylko przy realnej zmianie. - Hash w nazwie pliku rozwiązuje wdrożenia. Pliki z hashem dostają
immutablena rok, HTML dostajeno-cache. Maksymalny cache i natychmiastowe wdrożenia naraz.
Następnym razem, gdy wdrożysz zmianę i zobaczysz starą wersję, nie czyść cache ręcznie - sprawdź w zakładce Network, jaki Cache-Control ma twój HTML i czy pliki mają hash w nazwie. To nie przeglądarka się myli - to nagłówki, które sam ustawiłeś, mówią jej, że stara kopia jest wciąż dobra.
Komentarze
Wczytywanie komentarzy…