Cache przeglądarki — cichy bohater wydajności

Kiedy wchodzisz na stronę internetową po raz drugi, większość zasobów ładuje się niemal natychmiast. To zasługa cache przeglądarki — mechanizmu, który przechowuje lokalne kopie pobranych plików i serwuje je bez kontaktu z serwerem. Poprawnie skonfigurowany cache potrafi zredukować czas ładowania strony o 80-90% przy kolejnych wizytach.

Problem w tym, że domyślna konfiguracja cache rzadko jest optymalna. Zbyt agresywne buforowanie sprawia, że użytkownicy widzą nieaktualne treści. Zbyt słabe — że strona ładuje się wolno. Klucz leży w zrozumieniu nagłówków HTTP odpowiedzialnych za cache.

Cache-Control — główny mechanizm sterowania

Nagłówek Cache-Control to najważniejszy i najpotężniejszy mechanizm kontroli buforowania w HTTP/1.1. Składa się z dyrektyw oddzielonych przecinkami:

  • max-age=3600 — zasób jest ważny przez 3600 sekund (1 godzinę). W tym czasie przeglądarka użyje lokalnej kopii bez pytania serwera.
  • public — zasób może być buforowany przez wszystkie cache'e (przeglądarkę, CDN, proxy). Stosujemy dla plików statycznych.
  • private — tylko przeglądarka użytkownika może buforować zasób. Ważne dla danych osobistych — np. strona profilu.
  • no-cache — przeglądarka przechowuje kopię, ale za każdym razem pyta serwer, czy jest aktualna. Wbrew nazwie, nie oznacza braku cache'owania.
  • no-store — całkowity zakaz buforowania. Zasób nie jest przechowywany nigdzie. Stosujemy dla danych wrażliwych.
  • immutable — zasób nigdy się nie zmieni. Przeglądarka nie będzie weryfikować ważności nawet po odświeżeniu strony. Idealne dla plików z hashem w nazwie.

Przykładowa konfiguracja w Nginx dla plików statycznych z hashem wersji:

location ~* \.(css|js)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

Rok cache'owania (max-age=31536000) z immutable — bezpieczne, bo zmiana pliku oznacza zmianę nazwy (np. style.a1b2c3.css).

ETag — cyfrowy odcisk palca zasobu

ETag (Entity Tag) to unikalny identyfikator konkretnej wersji zasobu. Serwer generuje go na podstawie zawartości pliku — najczęściej jest to hash MD5 lub SHA, ale może to być dowolny ciąg znaków.

Mechanizm działania jest elegancki:

  1. Przy pierwszym żądaniu serwer odpowiada nagłówkiem ETag: "a1b2c3d4"
  2. Przeglądarka zapisuje zasób wraz z ETagiem
  3. Przy kolejnym żądaniu przeglądarka wysyła If-None-Match: "a1b2c3d4"
  4. Serwer porównuje ETag — jeśli się zgadza, odpowiada 304 Not Modified (bez treści)
  5. Przeglądarka używa lokalnej kopii

Odpowiedź 304 jest minimalna — kilkadziesiąt bajtów zamiast kilobajtów czy megabajtów danych. Oszczędność transferu jest ogromna, choć sam round-trip do serwera nadal występuje.

W Go generowanie ETagu jest proste:

func generateETag(content []byte) string {
    hash := sha256.Sum256(content)
    return fmt.Sprintf(`"%x"`, hash[:8])
}

Last-Modified — starszy brat ETagu

Nagłówek Last-Modified zawiera datę ostatniej modyfikacji zasobu. Działa analogicznie do ETag, ale zamiast porównywania hashów, porównywane są daty:

  1. Serwer odpowiada: Last-Modified: Tue, 02 Sep 2025 10:00:00 GMT
  2. Przeglądarka przy kolejnym żądaniu wysyła: If-Modified-Since: Tue, 02 Sep 2025 10:00:00 GMT
  3. Serwer sprawdza datę — jeśli zasób nie był modyfikowany, zwraca 304

Last-Modified ma niższą precyzję niż ETag — rozdzielczość to jedna sekunda. Jeśli plik zmienia się częściej, ETag jest lepszym wyborem. W praktyce oba nagłówki często stosuje się jednocześnie — ETag ma wyższy priorytet.

Strategie cache'owania

Różne typy zasobów wymagają różnych strategii. Oto podejście, które stosujemy w naszych projektach:

Pliki statyczne z wersjonowaniem (CSS, JS z hashem w nazwie):

  • Cache-Control: public, max-age=31536000, immutable
  • Brak potrzeby rewalidacji — zmiana pliku = nowa nazwa

Pliki statyczne bez wersjonowania (favicon, robots.txt):

  • Cache-Control: public, max-age=86400
  • ETag do rewalidacji po wygaśnięciu
  • Cache na 24 godziny z weryfikacją

Strony HTML:

  • Cache-Control: no-cache
  • ETag lub Last-Modified do rewalidacji
  • Przeglądarka zawsze sprawdza aktualność, ale używa cache przy 304

Odpowiedzi API:

  • Cache-Control: private, max-age=0, must-revalidate
  • Dla danych publicznych: Cache-Control: public, max-age=300 (5 minut)
  • Dla danych osobistych: Cache-Control: private, no-store

Obrazy (zdjęcia, ikony):

  • Cache-Control: public, max-age=2592000 (30 dni)
  • ETag do rewalidacji
  • Obrazy zmieniają się rzadko, ale nie chcemy immutable bez wersjonowania

Nagłówek Vary — cache a negocjacja treści

Nagłówek Vary informuje cache, że odpowiedź zależy od wartości określonego nagłówka żądania. Najczęstsze zastosowanie:

Vary: Accept-Encoding

To mówi cache'owi: "odpowiedź jest inna dla klientów akceptujących gzip i tych, którzy go nie obsługują". Bez tego nagłówka cache mógłby podać skompresowaną wersję klientowi, który nie obsługuje kompresji.

Inne zastosowania Vary to negocjacja języka (Vary: Accept-Language) czy formatu obrazu (Vary: Accept — WebP vs JPEG).

Debugowanie cache

Narzędzia deweloperskie przeglądarki (zakładka Network) to najlepszy sposób na weryfikację cache. Zwracaj uwagę na:

  • Kolumna Status — 200 (pobrany z serwera), 304 (rewalidacja), 200 (from cache) — pobrany z cache bez kontaktu z serwerem
  • Nagłówki odpowiedzi — sprawdź Cache-Control, ETag, Last-Modified
  • Rozmiar — odpowiedź 304 powinna mieć minimalny rozmiar

Polecenie curl -I jest przydatne do szybkiej weryfikacji nagłówków z linii poleceń:

curl -I https://example.com/style.css

Podsumowanie

Poprawna konfiguracja nagłówków cache to jedno z najskuteczniejszych narzędzi optymalizacji wydajności stron internetowych. Kombinacja Cache-Control z ETag lub Last-Modified pozwala precyzyjnie kontrolować, co i jak długo jest buforowane. Kluczowa zasada: pliki z hashem w nazwie cache'uj agresywnie, strony HTML rewaliduj przy każdym żądaniu.

Chcesz zoptymalizować wydajność swojej strony? Skontaktuj się z nami — przeanalizujemy konfigurację cache i zaproponujemy ulepszenia.