Cache-Control — nagłówek HTTP, który określa zasady buforowania zasobów po stronie klienta, serwerów proxy i CDN za pomocą zestawu dyrektyw. W przeciwieństwie do przestarzałego nagłówka Expires, Cache-Control obsługuje dziesiątki kombinacji: max-age określa czas życia w sekundach, private i public zarządzają dostępnością pamięci podręcznej, no-cache i no-store — wymuszonym sprawdzaniem. Według Google Web Dev (2025), prawidłowa konfiguracja Cache-Control może skrócić czas ładowania stron o 50-80% przy ponownych odwiedzinach. To czyni nagłówek krytycznym dla wydajności aplikacji internetowych i mobilnych.
Najważniejsze
Cache-Control — to nagłówek HTTP, ustandaryzowany w HTTP/1.1 (RFC 7234), który pozwala serwerowi określić, jak i przez jaki czas klienci, proxy i CDN mogą buforować odpowiedź. W przeciwieństwie do Expires (HTTP/1.0), Cache-Control używa dyrektyw — poleceń tekstowych, które łączy się przecinkami: Cache-Control: public, max-age=3600, must-revalidate. Nagłówek daje precyzyjną kontrolę nad każdym ogniwem łańcucha buforowania.
Buforowanie to jeden z fundamentalnych mechanizmów wydajności sieci i aplikacji mobilnych. Bez niego każde żądanie użytkownika szłoby bezpośrednio do serwera, powodując nadmierne obciążenie i opóźnienia. Cache-Control określa trzy poziomy buforowania: przeglądarka/aplikacja (private cache), serwery proxy (shared cache) i CDN (distributed cache). Każdy poziom interpretuje dyrektywy na swój sposób.
Nieprawidłowa konfiguracja Cache-Control to jedna z najczęstszych przyczyn problemów z wydajnością. Zbyt agresywne buforowanie powoduje, że użytkownicy widzą nieaktualne dane. Zbyt słabe — prowadzi do nadmiernych żądań do serwera i wolnego ładowania. Według Akamai (2025), optymalizacja Cache-Control dla treści statycznych zmniejsza obciążenie serwera o 70-90% i poprawia czas ładowania o 40-60% dla użytkowników mobilnych.
Cache-Control pojawił się w HTTP/1.1 (RFC 2616, 1999) jako zamiennik Expires. Expires miał fundamentalny problem: używał absolutnej daty, która zależała od strefy czasowej serwera i klienta. Cache-Control rozwiązał ten problem, przechodząc na czas względny (max-age w sekundach od momentu otrzymania odpowiedzi). Później w RFC 7234 (2014) dodano nowe dyrektywy: immutable dla statyków, stale-while-revalidate i stale-if-error dla odroczonego sprawdzania.
Cache-Control zawiera ponad 10 dyrektyw podzielonych na trzy grupy: dyrektywy żądania (klient → serwer), dyrektywy odpowiedzi (serwer → klient) i rozszerzenia. W praktyce w programowaniu mobilnym używa się 6-7 podstawowych dyrektyw odpowiedzi, które pokrywają 95% scenariuszy buforowania. Omówmy każdą z przykładami i zaleceniami.
| Dyrektywa | Znaczenie | Przykład |
|---|---|---|
| max-age | Czas życia w sekundach od momentu odpowiedzi | max-age=3600 — 1 godzina |
| s-maxage | max-age dla shared cache (proxy, CDN) | s-maxage=86400 — 1 dzień dla CDN |
| public | Zezwala na buforowanie wszystkim (w tym proxy) | public, max-age=3600 |
| private | Zezwala na pamięć podręczną tylko przeglądarce/aplikacji | private, max-age=600 |
| no-cache | Nie używaj bez sprawdzenia (304 obowiązkowy) | no-cache |
| no-store | Całkowity zakaz buforowania | no-store |
| must-revalidate | Po max-age obowiązkowo sprawdź u origin | max-age=3600, must-revalidate |
| immutable | Zasób nie zmieni się (dla wersjonowanych statyków) | max-age=31536000, immutable |
max-age — najważniejsza dyrektywa. Zabrania klientowi wysyłania żądania do serwera przez określony czas. Dla statyków (CSS, JS, obrazy) max-age ustawia się zwykle od 1 dnia do 1 roku. Dla odpowiedzi API — od 0 sekund (dane zawsze świeże) do 5-10 minut (dane referencyjne). s-maxage pozwala ustawić różny czas życia dla CDN i przeglądarki: CDN przechowuje kopię 1 dzień, przeglądarka — 1 godzinę.
Te dwie dyrektywy są często mylone. no-cache nie zabrania buforowania — wymaga sprawdzania buforowanej kopii przy każdym użyciu przez żądanie warunkowe (If-Modified-Since lub If-None-Match). Jeśli serwer odpowiada 304 — klient używa pamięci podręcznej. Jeśli 200 — aktualizuje. no-store natomiast całkowicie zabrania zapisywania odpowiedzi w jakiejkolwiek pamięci podręcznej, w tym dyskowej i operacyjnej. Używaj no-store tylko dla danych wrażliwych — tokeny, dane płatnicze, dokumenty osobiste.
Nagłówek Expires (HTTP/1.0) również określa czas życia zasobu, ale używa absolutnej daty: Expires: Thu, 03 Jul 2026 12:00:00 GMT. Cache-Control max-age — czas względny od momentu odpowiedzi. Różnica jest krytyczna dla systemów rozproszonych: jeśli serwer i klient znajdują się w różnych strefach czasowych, Expires może być interpretowany nieprawidłowo. Cache-Control nie ma tego problemu — 3600 sekund to zawsze 3600 sekund.
Gdy oba nagłówki są obecne, Cache-Control ma priorytet nad Expires. Jest to określone w RFC 7234: „Jeśli odpowiedź zawiera Cache-Control z dyrektywą max-age, odbiorca MUSI zignorować Expires”. W praktyce zaleca się nie zwracać Expires dla nowoczesnych klientów, ponieważ Cache-Control pokrywa wszystkie scenariusze Expires. Jednak dla zachowania wstecznej kompatybilności ze starymi proxy i przeglądarkami można zwracać oba nagłówki.
Expires zachował się głównie dla treści statycznych na Nginx i Apache — te serwery automatycznie dodają oba nagłówki. Jeśli w twoim projekcie występuje Expires bez Cache-Control, zastąp go Cache-Control z max-age: precyzja zarządzania pamięcią podręczną wzrasta, a zależność od strefy czasowej zostaje wyeliminowana. Do migracji wystarczy skonfigurować serwer tak, aby dodawał Cache-Control zamiast Expires.
# Nginx: Cache-Control dla plików statycznych
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 30d;
add_header Cache-Control "public, immutable, max-age=2592000";
}
# Różne polityki dla różnych typów treści
location /api/config {
expires -1;
add_header Cache-Control "no-cache, must-revalidate";
}
location /api/static-data {
expires 5m;
add_header Cache-Control "public, max-age=300";
}
W konfiguracji Nginx dla plików statycznych (CSS, JS, obrazy) ustawiany jest Cache-Control na 30 dni z atrybutem immutable — ten atrybut informuje przeglądarkę, że zasób nigdy nie zmienia się pod tym adresem URL (wersjonowanie przez hash w nazwie pliku). Punkty końcowe API używają no-cache dla danych dynamicznych i public z krótkim max-age dla referencyjnych — często żądanych i rzadko zmieniających się list.
W aplikacjach mobilnych Cache-Control odgrywa szczególną rolę ze względu na ograniczenia sieci mobilnych: wysokie opóźnienie, niestabilne połączenie, limity transferu. Prawidłowe buforowanie pozwala użytkownikowi widzieć dane natychmiast, nawet offline, i aktualizować je w tle. OkHttp na Androidzie i URLSession na iOS mają wbudowane systemy buforowania uwzględniające Cache-Control.
OkHttp używa CacheInterceptor, który odczytuje Cache-Control z odpowiedzi i automatycznie zarządza buforowaniem. Jeśli serwer zwrócił Cache-Control: max-age=3600, OkHttp nie będzie wysyłać żądania do serwera przez godzinę. Po wygaśnięciu max-age OkHttp wysyła żądanie warunkowe z If-Modified-Since i If-None-Match. Konfiguracja pamięci podręcznej w OkHttp: OkHttpClient.Builder().cache(Cache(directory, maxSize)).
fun createCachedClient(cacheDir: File): OkHttpClient {
return OkHttpClient.Builder()
.cache(Cache(cacheDir, 10L * 1024 * 1024))
.addNetworkInterceptor { chain ->
val response = chain.proceed(chain.request())
response.newBuilder()
.header("Cache-Control",
"public, max-age=300")
.removeHeader("Pragma")
.build()
}
.build()
}
Kod tworzy OkHttpClient z pamięcią podręczną 10 MB i nadpisuje Cache-Control przez NetworkInterceptor. Jeśli serwer nie zwraca Cache-Control lub używa Expires, intercept dodaje public, max-age=300 (5 minut). Interceptor usuwa przestarzały nagłówek Pragma (HTTP/1.0) dla zgodności. Według analogicznego schematu działa buforowanie na iOS przez URLCache.shared z konfiguracją memoryCapacity i diskCapacity.
Dyrektywa stale-while-revalidate pozwala użytkownikowi wyświetlać nieaktualną pamięć podręczną (stale), podczas gdy aplikacja w tle ładuje świeże dane. Daje to efekt natychmiastowej odpowiedzi: użytkownik widzi treść od razu, a po sekundzie jest ona aktualizowana. Obsługiwane przez OkHttp od wersji 3.10 i URLCache na iOS 14+. Przykład: Cache-Control: max-age=3600, stale-while-revalidate=300 — 1 godzina aktualnej pamięci podręcznej, potem 5 minut wyświetlania nieaktualnej z aktualizacją w tle.
Różne typy zasobów wymagają różnych strategii buforowania. Omówmy optymalne konfiguracje dla typowych scenariuszy w programowaniu mobilnym. Dla treści statycznych z hashem w nazwie pliku (bundle.abc123.js) można ustawić max-age do 1 roku z immutable. Dla list API, które aktualizują się rzadko (katalogi, kategorie), — max-age od 5 minut do 1 godziny z stale-while-revalidate.
| Typ zasobu | Cache-Control | Wyjaśnienie |
|---|---|---|
| Wersjonowane statyki | public, max-age=31536000, immutable | 1 rok, pliki się nie zmieniają (hash w URL) |
| Niewersjonowane statyki | public, max-age=86400, must-revalidate | 1 dzień z obowiązkowym sprawdzeniem po |
| API: dane referencyjne | public, max-age=600, stale-while-revalidate=60 | 10 minut pamięci podręcznej + 1 minuta stale |
| API: dane użytkownika | private, max-age=60 | 1 minuta, tylko dla konkretnego użytkownika |
| API: dane wrażliwe | no-store | Całkowity zakaz buforowania |
| Strony HTML | no-cache, must-revalidate | Sprawdzanie przy każdym żądaniu, 304 przy braku zmian |
Ważne jest, aby pamiętać o bezpieczeństwie: dla odpowiedzi zawierających dane osobowe użytkownika zawsze ustawiaj private. Bez tej dyrektywy publiczne proxy (np. korporacyjne) może buforować odpowiedź i przekazać ją innemu użytkownikowi. Dla tokenów uwierzytelniania i informacji płatniczych używaj no-store — nawet private cache nie powinien zapisywać tych danych na dysk.
Aby sprawdzić poprawność Cache-Control, użyj nagłówka Age (ile sekund pamięć podręczna jest przechowywana) i X-Cache (hit/miss na CDN). W przeglądarce — zakładka Network, kolumna Size pokazuje „from disk cache” lub „304 Not Modified”. Jeśli zasób powinien być buforowany, ale ładuje się za każdym razem — sprawdź, czy serwer nie dodaje Cache-Control: no-cache lub Pragma: no-cache razem z twoimi dyrektywami.
Często zadawane pytania
max-age działa dla wszystkich pamięci podręcznych (w tym przeglądarek), s-maxage — tylko dla shared cache (proxy, CDN). Jeśli podano s-maxage, CDN ignoruje max-age i używa s-maxage. Pozwala to ustawić różny czas życia dla przeglądarki i CDN.
Nie, po wysłaniu odpowiedzi z max-age klient nie będzie wysyłać żądania do wygaśnięcia timera. Do natychmiastowej unieważnienia pamięci podręcznej należy zmienić URL zasobu (dodać wersję/hash) i rozesłać powiadomienia push lub wiadomości WebSocket w celu wymuszonego resetu.
Dyrektywa immutable (RFC 8246) informuje przeglądarkę, że zasób nigdy się nie zmieni pod danym adresem URL. Przeglądarka nawet nie próbuje wysłać żądania warunkowego przy odświeżaniu strony — używa pamięci podręcznej do wygaśnięcia max-age. Działa tylko z wersjonowanymi plikami.
Googlebot uwzględnia Cache-Control: długie buforowanie przyspiesza ponowne skanowanie. noindex z szybką pamięcią podręczną — ok. no-store może spowolnić indeksowanie, ponieważ Googlebot będzie ładować stronę za każdym razem od zera. Zbyt krótki max-age zwiększa obciążenie serwera podczas skanowania.
Przez helmet lub middleware: res.set('Cache-Control', 'public, max-age=3600'). Dla statyków użyj express.static z parametrem maxAge: express.static('public', {maxAge: '1y'}). Dla dynamicznych tras — indywidualnie w każdym handlerze.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również