Dlaczego cachowanie jest kluczowe?
Każde żądanie HTTP to łańcuch operacji: rozwiązanie DNS, nawiązanie połączenia, przetworzenie żądania na serwerze, zapytanie do bazy danych, renderowanie odpowiedzi i przesłanie jej do klienta. Cachowanie pozwala skrócić lub całkowicie pominąć część tych kroków, serwując wcześniej przygotowaną odpowiedź.
Dobrze skonfigurowany cache może kilkudziesięciokrotnie przyspieszyć ładowanie strony, zmniejszyć obciążenie serwera i obniżyć koszty infrastruktury. Źle skonfigurowany — powoduje wyświetlanie nieaktualnych danych i trudne do zdiagnozowania błędy.
Cache przeglądarki (HTTP Cache)
Pierwsza i najbliższa użytkownikowi warstwa cache to przeglądarka. Serwer za pomocą nagłówków HTTP informuje przeglądarkę, jak długo może przechowywać zasoby lokalnie.
Cache-Control
Najważniejszy nagłówek kontrolujący cachowanie:
Cache-Control: public, max-age=31536000, immutable
- public — zasób może być cachowany przez przeglądarkę i pośredników (CDN, proxy)
- max-age=31536000 — zasób jest ważny przez rok (w sekundach)
- immutable — zasób nigdy się nie zmieni (nie sprawdzaj ponownie)
Dla zasobów statycznych (CSS, JS, obrazy) z hashami w nazwie pliku (style.a1b2c3.css) stosujemy długi max-age z immutable. Dla stron HTML — krótki cache lub brak:
Cache-Control: no-cache
no-cache nie oznacza braku cachowania — oznacza, że przeglądarka musi zweryfikować aktualność zasobu z serwerem przed użyciem kopii z cache.
ETag i Last-Modified
Gdy przeglądarka weryfikuje aktualność zasobu, serwer może odpowiedzieć kodem 304 Not Modified zamiast przesyłać cały zasób ponownie:
ETag: "abc123"
Last-Modified: Tue, 22 Oct 2024 10:00:00 GMT
Przeglądarka przy kolejnym żądaniu wysyła:
If-None-Match: "abc123"
If-Modified-Since: Tue, 22 Oct 2024 10:00:00 GMT
Jeśli zasób się nie zmienił, serwer odpowiada pustym 304 — oszczędzając transfer.
Konfiguracja w Nginx
# Zasoby statyczne z hashami — cache na rok
location ~* \.(css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Obrazy — cache na miesiąc
location ~* \.(jpg|jpeg|png|webp|svg|ico)$ {
expires 30d;
add_header Cache-Control "public";
}
# HTML — bez cache lub krótki
location ~* \.html$ {
add_header Cache-Control "no-cache";
}
CDN — Content Delivery Network
CDN to sieć serwerów rozmieszczonych geograficznie, które przechowują kopie zasobów blisko użytkownika. Zamiast pobierać obraz z serwera w Warszawie, użytkownik z Londynu pobiera go z najbliższego węzła CDN.
Kiedy warto używać CDN?
- Strona ma użytkowników z różnych regionów geograficznych
- Serwujesz dużo zasobów statycznych (obrazy, filmy, pliki CSS/JS)
- Chcesz odciążyć serwer główny od obsługi statyki
Popularne usługi CDN
- Cloudflare — darmowy plan z podstawowym CDN, ochroną DDoS i SSL
- BunnyCDN — tani i szybki, rozliczanie za transfer
- AWS CloudFront — integracja z ekosystemem AWS
Dla małych stron firmowych Cloudflare w darmowym planie jest zazwyczaj wystarczający i oferuje dodatkowe korzyści: ochronę DDoS, automatyczną minifikację i darmowy SSL.
Cache po stronie serwera
Cache w pamięci aplikacji
Najprostsza forma cache serwerowego to przechowywanie wyników w pamięci aplikacji:
var (
pageCache = make(map[string]cachedPage)
mu sync.RWMutex
)
type cachedPage struct {
content []byte
expiresAt time.Time
}
func getCachedPage(key string) ([]byte, bool) {
mu.RLock()
defer mu.RUnlock()
page, exists := pageCache[key]
if !exists || time.Now().After(page.expiresAt) {
return nil, false
}
return page.content, true
}
Ta metoda sprawdza się dla prostych aplikacji z jedną instancją. Problemy pojawiają się przy wielu instancjach — każda ma własną kopię cache.
Cache szablonów
Renderowanie szablonów HTML to operacja, która może być kosztowna przy dużej liczbie żądań. Parsowanie szablonów przy starcie aplikacji zamiast przy każdym żądaniu to podstawowa optymalizacja:
var templates = template.Must(template.ParseGlob("templates/*.html"))
Redis jako warstwa cache
Redis to baza danych in-memory, idealna jako współdzielona warstwa cache dla aplikacji:
- Przechowuje dane w pamięci RAM — odczyty w mikrosekundach
- Obsługuje TTL (time-to-live) — automatyczne wygasanie wpisów
- Współdzielony między instancjami aplikacji
- Obsługuje struktury danych: stringi, hashe, listy, zbiory
Typowe zastosowania Redis jako cache:
- Wyniki zapytań do bazy danych — szczególnie złożonych, rzadko zmieniających się
- Sesje użytkowników — szybki dostęp, automatyczne wygasanie
- Wyrenderowane fragmenty HTML — np. sidebar, menu nawigacyjne
- Wyniki API zewnętrznych — ograniczenie liczby zapytań do zewnętrznych usług
Redis zużywa pamięć RAM, dlatego ważne jest ustawianie TTL dla wszystkich kluczy i monitorowanie zużycia pamięci. Konfiguracja polityki usuwania (maxmemory-policy) określa, co dzieje się po przekroczeniu limitu pamięci — najczęściej stosuje się allkeys-lru (usuwanie najrzadziej używanych kluczy).
Strategie invalidacji cache
Najtrudniejszym aspektem cachowania jest invalidacja — usuwanie lub aktualizowanie nieaktualnych danych. Popularne strategie:
- TTL (Time-To-Live) — cache wygasa po określonym czasie. Proste, ale dane mogą być chwilowo nieaktualne.
- Cache-aside — aplikacja najpierw sprawdza cache; przy braku trafienia pobiera dane ze źródła i zapisuje w cache.
- Write-through — przy zapisie danych aktualizowany jest jednocześnie cache i źródło.
- Event-driven — cache jest czyszczony w odpowiedzi na zdarzenia (np. aktualizacja artykułu czyści cache strony).
Podsumowanie
Cachowanie to nie pojedyncze rozwiązanie, a zestaw warstw — od przeglądarki, przez CDN, po cache serwerowy i Redis. Każda warstwa ma swoje zastosowanie i ograniczenia. Najważniejsza zasada: zacznij od prawidłowej konfiguracji nagłówków HTTP cache dla zasobów statycznych — to daje największy efekt przy najmniejszym nakładzie pracy. Kolejne warstwy dodawaj stopniowo, mierząc ich wpływ na wydajność. Pamiętaj, że przedwczesna optymalizacja cachowania potrafi skomplikować architekturę bez wymiernych korzyści.