Przejdź do treści

HTTP caching: czemu druga wizyta na stronie jest natychmiastowa

Profile photo of Adrian Zawadzki

Adrian Zawadzki

9 min czytania

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=3600

s-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.

Świeżość kopii w cache
cache pustyCzas: 0 s
Przeglądarkabrak kopii
Serwer (origin)wydaje "v1-abc"
Co poleciało 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.css

Zmienisz 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-cache

max-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=86400

Przez 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:

  1. max-age to ś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ć”.
  2. Rewalidacja oszczędza transfer. Przeglądarka pyta ETag-iem, a serwer odpowiada 304 Not Modified bez body, jeśli nic się nie zmieniło. Pełne pobranie tylko przy realnej zmianie.
  3. Hash w nazwie pliku rozwiązuje wdrożenia. Pliki z hashem dostają immutable na rok, HTML dostaje no-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…

Dodaj komentarz