Wprowadzenie: cachowanie jako fundament wydajności webowej
Najszybsze żądanie HTTP to takie, które nigdy nie zostało wysłane. Brzmi jak truizm, ale ta prosta zasada stoi za jedną z najskuteczniejszych technik optymalizacji wydajności stron internetowych — cachowaniem. W poprzednich artykułach omówiliśmy optymalizację Core Web Vitals i redukcję rozmiarów bundli JavaScript. Teraz pora na trzeci, absolutnie kluczowy filar wydajności: wielowarstwowe strategie cachowania.
Dane z HTTP Archive za początek 2026 roku nie kłamią — mediana strony mobilnej wykonuje ponad 75 żądań HTTP, pobierając łącznie ponad 2,2 MB danych. Nawet po doskonałej optymalizacji bundli i obrazów, jeśli przeglądarka musi pobierać te same zasoby przy każdej wizycie — marnujesz przepustowość, czas użytkownika i pieniądze na infrastrukturę. Dobrze zaprojektowana strategia cachowania potrafi zredukować liczbę żądań o 60–80%, a czas ładowania powtórnych wizyt skrócić do ułamka sekundy.
Dobra, to co nas czeka? W tym artykule pokażę Ci kompletny obraz nowoczesnego cachowania: od nagłówków HTTP Cache-Control, przez Service Workers z biblioteką Workbox, po strategię CDN i rewolucyjne Speculation Rules API. Na końcu każdej sekcji znajdziesz gotowe fragmenty kodu, które możesz wdrożyć od razu.
Warstwy cachowania: zrozumienie pełnego obrazu
Zanim przejdziemy do szczegółów, warto zrozumieć jedną rzecz — cachowanie w nowoczesnym webie działa na wielu poziomach jednocześnie. Każda warstwa ma inne zadanie, inne możliwości i inne kompromisy:
- Cache przeglądarki (HTTP Cache) — wbudowany mechanizm przeglądarki, który przechowuje zasoby lokalnie na podstawie nagłówków odpowiedzi serwera. To najprostsza i najbardziej podstawowa warstwa.
- Service Worker Cache (Cache API) — programowalny cache kontrolowany przez JavaScript, który daje pełną kontrolę nad tym, co, kiedy i jak jest cachowane. Umożliwia też działanie offline.
- Cache CDN (Edge Cache) — zasoby przechowywane na serwerach brzegowych sieci CDN, bliżej użytkownika geograficznie. Zmniejsza obciążenie serwera origin i redukuje latencję.
- Cache serwera (Reverse Proxy) — cache na poziomie serwera (np. Varnish, Nginx), który przechowuje wyrenderowane strony i odpowiedzi API, odciążając backend.
Te warstwy działają kaskadowo. Żądanie przeglądarki najpierw sprawdza Service Worker Cache, potem HTTP Cache, następnie CDN, a na końcu dociera do serwera origin. Im lepiej zaprojektujesz strategię, tym mniej żądań dotrze do tych dalszych (i wolniejszych) warstw.
HTTP Cache-Control: fundament każdej strategii cachowania
Nagłówek Cache-Control to absolutna podstawa kontroli cachowania w protokole HTTP. Pomimo swojej pozornej prostoty, jest zaskakująco często źle rozumiany i źle konfigurowany. Szczerze mówiąc, widziałem to dziesiątki razy w audytach — nawet doświadczeni deweloperzy potrafią tu namieszać.
Dyrektywy Cache-Control — kompletny przegląd
Oto najważniejsze dyrektywy, które musisz znać w 2026 roku:
- max-age=N — odpowiedź jest świeża przez N sekund od wygenerowania. Po tym czasie staje się „stale" (nieaktualna).
- s-maxage=N — nadpisuje
max-agewyłącznie dla cache'ów współdzielonych (CDN, reverse proxy). Nie dotyczy przeglądarki. - no-cache — zasób może być przechowywany w cache, ale musi być rewalidowany z serwerem przy każdym użyciu. Uwaga: to nie oznacza „nie cachuj"!
- no-store — zasób nie powinien być w ogóle przechowywany w żadnym cache. Używaj dla danych wrażliwych.
- private — zasób może być cachowany tylko przez przeglądarkę użytkownika, nie przez CDN ani proxy.
- public — zasób może być cachowany przez wszystkie warstwy, włącznie z CDN.
- immutable — zawartość nigdy się nie zmieni — przeglądarka nie musi rewalidować nawet przy odświeżaniu strony.
- must-revalidate — po wygaśnięciu
max-agecache musi rewalidować z serwerem, zanim użyje nieaktualnej kopii. - stale-while-revalidate=N — pozwala serwować nieaktualną kopię przez N sekund, jednocześnie rewalidując w tle.
- stale-if-error=N — pozwala serwować nieaktualną kopię przez N sekund, jeśli serwer zwraca błąd.
Wzorce cachowania dla różnych typów zasobów
Nie wszystkie zasoby powinny być cachowane tak samo — i to jest częsty błąd początkujących. Oto sprawdzone wzorce dla najpopularniejszych typów zasobów:
Zasoby statyczne z fingerprinting (JS, CSS, obrazy z hashem)
Pliki z hashem w nazwie (np. app.a1b2c3d4.js) to idealni kandydaci do agresywnego cachowania. Ponieważ każda zmiana kodu generuje nowy hash, stara wersja nigdy nie zostanie ponownie użyta:
# Nginx — konfiguracja dla zasobów z fingerprinting
location ~* \.(js|css)$ {
# Cachuj na rok — hash w nazwie gwarantuje świeżość
add_header Cache-Control "public, max-age=31536000, immutable";
# Kompresja Brotli z fallback na gzip
brotli on;
brotli_types text/javascript text/css;
gzip on;
gzip_types text/javascript text/css;
}
location ~* \.(png|jpg|jpeg|webp|avif|gif|ico|svg)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
Dyrektywa immutable jest tu kluczowa — mówi przeglądarce, że nie ma sensu rewalidować tego zasobu nawet przy twardym odświeżeniu (Ctrl+Shift+R). Oszczędza to niepotrzebne żądania 304 Not Modified. Mała rzecz, a cieszy.
Dokumenty HTML
Z kolei pliki HTML to zupełnie inna historia. Nie powinny być agresywnie cachowane, bo zawierają odniesienia do zasobów z nowymi hashami po każdym wdrożeniu:
# Nginx — konfiguracja dla dokumentów HTML
location ~* \.html$ {
add_header Cache-Control "no-cache";
# ETag pozwala na efektywną rewalidację (304 Not Modified)
etag on;
}
Użycie no-cache oznacza, że przeglądarka cachuje HTML, ale przy każdym żądaniu sprawdza z serwerem, czy wersja jest aktualna. Jeśli się nie zmieniła — serwer zwraca krótką odpowiedź 304 zamiast pełnego dokumentu. Proste i eleganckie.
Odpowiedzi API z dynamicznymi danymi
Dla odpowiedzi API kluczowe jest rozróżnienie między danymi publicznymi a prywatnymi:
# Dane publiczne, które rzadko się zmieniają (np. katalog produktów)
Cache-Control: public, max-age=300, s-maxage=600, stale-while-revalidate=86400
# Dane prywatne użytkownika (np. profil, koszyk)
Cache-Control: private, no-cache
# Dane wrażliwe (np. dane płatności, tokeny)
Cache-Control: no-store
Stale-While-Revalidate: idealny kompromis między świeżością a szybkością
Dyrektywa stale-while-revalidate to moim zdaniem najważniejsza innowacja w cachowaniu HTTP ostatnich lat. Pozwala na natychmiastowe serwowanie odpowiedzi z cache, jednocześnie odświeżając ją w tle. Użytkownik nigdy nie czeka — zawsze dostaje natychmiastową odpowiedź.
# Przykład: treść cachowana przez 10 minut,
# z oknem stale-while-revalidate na 24 godziny
Cache-Control: public, max-age=600, stale-while-revalidate=86400
# Co to oznacza w praktyce:
# 0-10 min: odpowiedź serwowana z cache (świeża)
# 10 min - 24h: odpowiedź serwowana z cache (nieaktualna),
# ale rewalidacja odbywa się w tle
# Po 24h: cache musi pobrać nową wersję z serwera
To podejście jest szczególnie skuteczne dla stron e-commerce, portali informacyjnych i aplikacji, gdzie dane zmieniają się regularnie, ale kilkuminutowe opóźnienie jest akceptowalne. W 2026 roku wszystkie główne przeglądarki w pełni wspierają tę dyrektywę, więc nie ma wymówek, żeby z niej nie korzystać.
ETag i Last-Modified: mechanizmy rewalidacji
Nagłówki ETag i Last-Modified działają razem z Cache-Control, zapewniając mechanizm rewalidacji. Kiedy cache wygaśnie, przeglądarka wysyła żądanie warunkowe z nagłówkiem If-None-Match (dla ETag) lub If-Modified-Since (dla Last-Modified). Serwer odpowiada albo kodem 304 (brak zmian — użyj cache), albo 200 z nową treścią.
// Express.js — konfiguracja ETag i cachowania
import express from 'express';
import { createHash } from 'crypto';
const app = express();
// Statyczne zasoby z fingerprinting — agresywne cachowanie
app.use('/assets', express.static('dist/assets', {
maxAge: '1y',
immutable: true,
etag: false, // Nie potrzebne przy immutable
}));
// HTML — zawsze rewaliduj
app.use(express.static('dist', {
maxAge: 0,
etag: true,
lastModified: true,
setHeaders: (res, path) => {
if (path.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache');
}
},
}));
// API z dynamicznymi danymi
app.get('/api/products', async (req, res) => {
const products = await getProducts();
const etag = createHash('md5')
.update(JSON.stringify(products))
.digest('hex');
res.setHeader('ETag', `"${etag}"`);
res.setHeader('Cache-Control',
'public, max-age=60, stale-while-revalidate=300');
// Sprawdź, czy klient ma aktualną wersję
if (req.headers['if-none-match'] === `"${etag}"`) {
return res.status(304).end();
}
res.json(products);
});
Service Workers i Cache API: pełna kontrola nad cachowaniem
HTTP Cache działa na podstawie nagłówków — to prosty, deklaratywny mechanizm. Ale co, jeśli potrzebujesz czegoś więcej? Na przykład: cachować odpowiedzi API z różnymi strategiami dla różnych endpointów, zapewnić działanie offline, albo dynamicznie aktualizować cache w tle? Tu na scenę wchodzi Service Worker z Cache API.
Czym jest Service Worker?
Service Worker to skrypt JavaScript działający w tle, niezależnie od strony internetowej. Działa jako proxy między przeglądarką a siecią — przechwytuje każde żądanie HTTP i może zdecydować, jak je obsłużyć: pobrać z sieci, z cache, lub połączyć oba podejścia.
Co ważne (i co czyni go naprawdę potężnym narzędziem), Service Worker działa nawet gdy strona jest zamknięta. Umożliwia to np. synchronizację w tle czy powiadomienia push.
Strategie cachowania w Service Workers
Istnieje pięć głównych strategii cachowania, z których każda odpowiada innym wymaganiom. Przejdźmy przez trzy najważniejsze:
1. Cache First (Offline First)
Najpierw sprawdza cache, a do sieci sięga tylko gdy zasobu nie ma w cache. Idealna dla zasobów statycznych, które rzadko się zmieniają.
// Service Worker — strategia Cache First
self.addEventListener('fetch', (event) => {
// Stosuj Cache First tylko dla zasobów statycznych
if (event.request.destination === 'image' ||
event.request.destination === 'style' ||
event.request.destination === 'script') {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse; // Zwróć z cache
}
// Brak w cache — pobierz z sieci i zapisz
return fetch(event.request).then((networkResponse) => {
const responseClone = networkResponse.clone();
caches.open('static-v1').then((cache) => {
cache.put(event.request, responseClone);
});
return networkResponse;
});
})
);
}
});
2. Network First
Odwrotna logika — najpierw próbuje pobrać z sieci, a cache używa jako fallback. Idealna dla treści dynamicznych: artykułów, danych z API czy stron HTML.
// Service Worker — strategia Network First
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request)
.then((networkResponse) => {
// Zapisz świeżą odpowiedź w cache
const responseClone = networkResponse.clone();
caches.open('api-v1').then((cache) => {
cache.put(event.request, responseClone);
});
return networkResponse;
})
.catch(() => {
// Sieć niedostępna — zwróć z cache
return caches.match(event.request);
})
);
}
});
3. Stale-While-Revalidate
Mój osobisty faworyt. Zwraca odpowiedź z cache natychmiast, jednocześnie pobierając świeżą wersję z sieci do cache. Użytkownik dostaje błyskawiczną odpowiedź, a przy następnym żądaniu — już aktualną wersję.
// Service Worker — strategia Stale-While-Revalidate
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open('swr-cache').then((cache) => {
return cache.match(event.request).then((cachedResponse) => {
// Zawsze pobieraj z sieci w tle
const fetchPromise = fetch(event.request).then(
(networkResponse) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
}
);
// Zwróć cache od razu, jeśli dostępny
return cachedResponse || fetchPromise;
});
})
);
});
Workbox: profesjonalne cachowanie bez boilerplate'u
Ręczne pisanie logiki Service Workera to, powiedzmy sobie wprost, żmudna i podatna na błędy robota. Dlatego w 2026 roku standardem branżowym jest Workbox — biblioteka od Google, która abstrakcjonuje złożoność Service Workers i oferuje gotowe, przetestowane strategie cachowania. Z Workbox korzysta ponad 54% stron mobilnych — i nie bez powodu.
// sw.js — konfiguracja Workbox
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import {
CacheFirst,
NetworkFirst,
StaleWhileRevalidate
} from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from
'workbox-cacheable-response';
// 1. Precaching — zasoby krytyczne cachowane przy instalacji
precacheAndRoute(self.__WB_MANIFEST);
// 2. Obrazy — Cache First z limitem 60 sztuk i 30 dni
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images-cache',
plugins: [
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 dni
}),
new CacheableResponsePlugin({
statuses: [0, 200],
}),
],
})
);
// 3. Zasoby JS/CSS — Stale-While-Revalidate
registerRoute(
({ request }) =>
request.destination === 'script' ||
request.destination === 'style',
new StaleWhileRevalidate({
cacheName: 'static-resources',
plugins: [
new ExpirationPlugin({
maxEntries: 30,
maxAgeSeconds: 7 * 24 * 60 * 60, // 7 dni
}),
],
})
);
// 4. Odpowiedzi API — Network First z fallback
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api-cache',
networkTimeoutSeconds: 3, // Timeout 3s, potem cache
plugins: [
new ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 24 * 60 * 60, // 24 godziny
}),
],
})
);
// 5. Strony HTML — Network First
registerRoute(
({ request }) => request.mode === 'navigate',
new NetworkFirst({
cacheName: 'pages-cache',
plugins: [
new ExpirationPlugin({
maxEntries: 25,
maxAgeSeconds: 7 * 24 * 60 * 60,
}),
],
})
);
Integracja Workbox z Vite
W ekosystemie Vite 8 integracja z Workbox jest na szczęście prosta dzięki pluginowi vite-plugin-pwa:
// vite.config.ts — integracja z PWA/Workbox
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate',
workbox: {
// Precaching zasobów z builda
globPatterns: [
'**/*.{js,css,html,ico,png,svg,woff2}'
],
// Runtime caching
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 50,
maxAgeSeconds: 86400,
},
networkTimeoutSeconds: 3,
},
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp|avif)$/,
handler: 'CacheFirst',
options: {
cacheName: 'image-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 30,
},
},
},
],
},
}),
],
});
Precaching vs. Runtime Caching — kiedy co stosować?
To pytanie, które dostaję naprawdę często. Odpowiedź zależy od charakteru zasobów:
- Precaching — używaj dla krytycznych zasobów znanych na etapie budowania (główny CSS, kluczowy JS, fonty, shell aplikacji). Zasoby są pobierane i cachowane podczas instalacji Service Workera, co gwarantuje ich dostępność offline od pierwszej wizyty.
- Runtime Caching — używaj dla zasobów dynamicznych, zasobów zależnych od użytkownika, dużych plików i treści API. Zasoby są cachowane dopiero przy pierwszym żądaniu.
Częsty błąd to nadmierne precachowanie — cachowanie zbyt wielu zasobów przy instalacji Service Workera powoduje wydłużenie czasu pierwszej wizyty i marnowanie transferu użytkownika. Precachuj tylko to, co jest naprawdę krytyczne. Reszta niech się cachuje w runtime.
Cachowanie na poziomie CDN: Edge Cache w praktyce
CDN (Content Delivery Network) to sieć serwerów brzegowych rozproszonych geograficznie, które przechowują kopie zasobów bliżej użytkowników. W 2026 roku CDN to już nie tylko serwowanie statycznych plików — nowoczesne CDN oferują Edge Computing, dynamiczną optymalizację obrazów, full page caching i zaawansowane reguły cachowania.
Konfiguracja Edge Cache z Cloudflare
Cloudflare jest najpopularniejszym CDN i oferuje naprawdę zaawansowane możliwości cachowania. Oto przykład konfiguracji z użyciem Cloudflare Workers:
// Cloudflare Worker — zaawansowane cachowanie na edge
export default {
async fetch(request, env) {
const url = new URL(request.url);
const cache = caches.default;
// Sprawdź cache na edge
let response = await cache.match(request);
if (!response) {
// Cache miss — pobierz z origin
response = await fetch(request);
// Ustaw nagłówki cachowania w zależności od typu zasobu
const contentType = response.headers.get('content-type');
const headers = new Headers(response.headers);
if (url.pathname.match(/\.(js|css|woff2)$/)) {
// Zasoby statyczne — cachuj na edge przez rok
headers.set('Cache-Control',
'public, max-age=31536000, immutable');
headers.set('CDN-Cache-Control',
'max-age=31536000');
} else if (contentType?.includes('text/html')) {
// HTML — krótki cache na edge, rewalidacja
headers.set('Cache-Control',
'public, max-age=60, s-maxage=300, ' +
'stale-while-revalidate=86400');
} else if (url.pathname.startsWith('/api/')) {
// API — różne strategie dla różnych endpointów
headers.set('Cache-Control',
'public, s-maxage=60, ' +
'stale-while-revalidate=300, ' +
'stale-if-error=86400');
}
response = new Response(response.body, {
status: response.status,
headers,
});
// Zapisz w edge cache
if (response.status === 200) {
await cache.put(request, response.clone());
}
}
return response;
},
};
CDN-Cache-Control: precyzyjne sterowanie cache'em CDN
W 2026 roku coraz większą popularność zyskują dedykowane nagłówki cachowania dla CDN. Pozwalają one na niezależne sterowanie cachowaniem na poziomie przeglądarki i CDN — i to jest naprawdę przydatne:
# Standardowy nagłówek — dotyczy wszystkich warstw
Cache-Control: public, max-age=60
# CDN-Cache-Control — nadpisuje Cache-Control tylko dla CDN
CDN-Cache-Control: max-age=3600
# Cloudflare-CDN-Cache-Control — specyficzny dla Cloudflare
Cloudflare-CDN-Cache-Control: max-age=7200
# Wynik: przeglądarka cachuje na 60s, CDN na 3600s (lub 7200s
# na Cloudflare). Pozwala agresywnie cachować na edge,
# zachowując krótki TTL w przeglądarce.
To rozwiązanie eliminuje problem „jednego rozmiaru dla wszystkich". Możesz agresywnie cachować na CDN (gdzie masz kontrolę nad inwalidacją), a zachować krótki cache w przeglądarce (gdzie nie masz bezpośredniej kontroli). Win-win.
Full Page Caching na edge
Jedną z najskuteczniejszych technik optymalizacji w 2026 roku jest full page caching — cachowanie całych wyrenderowanych stron HTML na serwerach edge. Zamiast renderować stronę przy każdym żądaniu na serwerze origin (co zajmuje 200–2000 ms), serwer edge zwraca gotowy HTML w 5–20 ms. Różnica jest odczuwalna gołym okiem.
// Next.js — konfiguracja ISR z edge caching
// app/products/[id]/page.tsx
export const revalidate = 3600; // Rewaliduj co godzinę
export async function generateStaticParams() {
const products = await getTopProducts(100);
return products.map((p) => ({ id: p.id.toString() }));
}
export default async function ProductPage({
params
}: { params: { id: string } }) {
const product = await getProduct(params.id);
return (
{product.name}
{product.description}
{/* Dynamiczne elementy ładowane client-side */}
}>
);
}
Wzorzec ISR (Incremental Static Regeneration) z Next.js idealnie współgra z edge cachingiem — strona jest renderowana raz, cachowana na CDN, a następnie automatycznie odświeżana w tle po upływie czasu revalidate. Dynamiczne elementy (np. aktualna cena) są ładowane po stronie klienta za pomocą Suspense.
Speculation Rules API: przyszłość nawigacji w webie
Speculation Rules API to jedno z najbardziej ekscytujących API wprowadzonych do Chromium w ostatnich latach. I nie, nie przesadzam z entuzjazmem. To API pozwala deklaratywnie powiedzieć przeglądarce, które strony powinna pobrać z wyprzedzeniem (prefetch) lub nawet w pełni wyrenderować w tle (prerender), zanim użytkownik na nie przejdzie. Efekt? Nawigacja w 0 ms.
Prefetch vs. Prerender
API oferuje dwa poziomy spekulatywnego ładowania:
- Prefetch — pobiera główny dokument HTML (i ewentualnie powiązane zasoby) do pamięci. Przy nawigacji strona wymaga jeszcze renderowania, ale oszczędzamy czas pobierania. Koszt: niski.
- Prerender — pobiera stronę, wykonuje cały JavaScript, renderuje DOM i trzyma gotową stronę w niewidocznej karcie. Przy nawigacji przeglądarka po prostu „aktywuje" gotową stronę. Efekt: nawigacja praktycznie w 0 ms. Koszt: wyższy (CPU i pamięć).
Implementacja Speculation Rules
Reguły definiuje się w bloku <script type="speculationrules"> w formacie JSON. Oto przykład, który sprawdza się w większości przypadków:
<script type="speculationrules">
{
"prefetch": [
{
"where": {
"and": [
{ "href_matches": "/*" },
{ "not": { "href_matches": "/logout" } },
{ "not": { "href_matches": "/api/*" } },
{ "not": { "selector_matches": "[data-no-prefetch]" } }
]
},
"eagerness": "moderate"
}
],
"prerender": [
{
"where": {
"and": [
{ "href_matches": "/*" },
{ "not": { "href_matches": "/checkout/*" } },
{ "not": { "href_matches": "/admin/*" } }
]
},
"eagerness": "moderate"
}
]
}
</script>
Poziomy eagerness
Parametr eagerness kontroluje, kiedy przeglądarka rozpoczyna spekulatywne ładowanie:
- immediate — rozpocznij natychmiast po załadowaniu reguł. Agresywne, ale kosztowne.
- eager — rozpocznij przy minimalnym sygnale zainteresowania (np. ruch kursora w kierunku linku).
- moderate — rozpocznij przy hover na linku. Domyślne i — moim zdaniem — najrozsądniejsze.
- conservative — rozpocznij dopiero przy kliknięciu (mousedown/pointerdown). Najpewniejsze, ale daje mniej czasu na przygotowanie.
Dynamiczne Speculation Rules z JavaScript
Reguły można również dodawać dynamicznie, co pozwala na bardziej zaawansowaną logikę — np. prerender na podstawie analityki czy zachowania użytkownika:
// Dynamiczne dodawanie Speculation Rules
function addSpeculationRules(urls) {
// Sprawdź wsparcie przeglądarki
if (!HTMLScriptElement.supports?.('speculationrules')) {
return; // Graceful degradation
}
const script = document.createElement('script');
script.type = 'speculationrules';
script.textContent = JSON.stringify({
prerender: [{
source: 'list',
urls: urls,
}],
});
document.head.appendChild(script);
}
// Prerender 3 najpopularniejszych podstron
// (na podstawie danych analitycznych)
addSpeculationRules([
'/produkty',
'/cennik',
'/kontakt',
]);
// Prerender po najechaniu na kategorię w menu
document.querySelectorAll('.nav-category').forEach((link) => {
link.addEventListener('mouseenter', () => {
addSpeculationRules([link.href]);
});
});
Limity i ograniczenia
Zanim rzucisz się do wdrażania, warto znać ograniczenia. Chrome nakłada limity na spekulatywne ładowanie, aby zapobiec nadmiernemu zużyciu zasobów:
- Maksymalnie 50 prefetchów i 10 prerenderów jednocześnie w pamięci.
- Chrome nie spekuluje w trybie oszczędzania energii, przy niskim stanie baterii ani przy ograniczonej pamięci.
- Prerender działa tylko dla stron z tego samego origin (same-origin). Cross-origin prerender wymaga nagłówka
Supports-Loading-Modepo stronie serwera. - Cross-site prefetch nie działa, jeśli użytkownik ma ustawione ciasteczka na docelowej stronie (ochrona prywatności).
Wsparcie przeglądarek
Na dzień dzisiejszy Speculation Rules API jest wspierany przez przeglądarki oparte na Chromium (Chrome, Edge, Opera). Firefox zmienił swoje stanowisko na „pozytywne" wobec prefetch, ale pełna implementacja jest jeszcze w toku. Safari? Jak zwykle — nie wspiera. Ale tu jest dobra wiadomość: Speculation Rules to progresywne ulepszenie — przeglądarki bez wsparcia po prostu ignorują blok <script type="speculationrules">, więc nic się nie psuje.
Inwalidacja cache: najtrudniejszy problem w informatyce
Phil Karlton powiedział kiedyś, że w informatyce są dwa najtrudniejsze problemy: nazewnictwo, inwalidacja cache'u i błędy „off-by-one". Żartobliwe, ale w tym żarcie jest sporo prawdy — inwalidacja cache to miejsce, gdzie większość strategii cachowania się komplikuje.
Wzorzec „Immutable Content + Mutable References"
Najskuteczniejszy wzorzec inwalidacji polega na rozdzieleniu zasobów na dwie kategorie:
- Zasoby niemutowalne (z hashem w nazwie) — cachowane „na wieczność" z
immutable. Nigdy nie wymagają inwalidacji, bo każda zmiana tworzy nowy plik z nowym hashem. - Referencje mutowalne (HTML, manifesty) — cachowane krótko lub z
no-cache. Wskazują na aktualne wersje zasobów niemutowalnych.
<!-- index.html — cachowany z no-cache, zawsze rewalidowany -->
<!DOCTYPE html>
<html>
<head>
<!-- Referencje do zasobów z hashem — aktualizowane przy
każdym deploymencie -->
<link rel="stylesheet" href="/assets/main.f4a8c2.css">
<script src="/assets/app.b7d3e1.js" defer></script>
</head>
<body>
<!-- Treść -->
<img src="/assets/hero.k9m2p4.avif" alt="Hero">
</body>
</html>
<!-- Po wdrożeniu nowej wersji: -->
<!-- main.f4a8c2.css → main.e5b9d3.css (nowy hash) -->
<!-- app.b7d3e1.js → app.c8f4a2.js (nowy hash) -->
<!-- Stary cache automatycznie przestaje być używany -->
Eleganckie, prawda? Ten wzorzec sprawdza się od lat i nie widać powodu, żeby z niego rezygnować.
Programowa inwalidacja cache CDN
Dla zasobów, które nie używają fingerprinting (np. strony HTML, odpowiedzi API), konieczna jest aktywna inwalidacja cache na CDN po wdrożeniu:
// Skrypt deploy — inwalidacja cache Cloudflare
async function purgeCloudflareCache(zoneId, apiToken, urls) {
const response = await fetch(
`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
files: urls, // Konkretne URL-e do inwalidacji
// lub: purge_everything: true (ostrożnie!)
}),
}
);
const result = await response.json();
if (!result.success) {
console.error('Purge failed:', result.errors);
}
return result;
}
// Po wdrożeniu — inwaliduj kluczowe strony
await purgeCloudflareCache(ZONE_ID, API_TOKEN, [
'https://example.com/',
'https://example.com/produkty',
'https://example.com/sitemap.xml',
]);
Monitorowanie i debugowanie cache
Nawet najlepsza strategia cachowania jest bezużyteczna, jeśli nie możesz zweryfikować, że działa prawidłowo. Serio — widziałem projekty, gdzie cała konfiguracja cache wyglądała świetnie na papierze, ale w praktyce nie działała z powodu jednego złego nagłówka.
Chrome DevTools — analiza cache w praktyce
Zakładka Network w Chrome DevTools to podstawowe narzędzie diagnostyczne. Kolumna „Size" pokazuje, skąd pochodzi zasób:
- (from disk cache) — zasób pochodzi z HTTP Cache przeglądarki (z dysku).
- (from memory cache) — zasób pochodzi z pamięci RAM przeglądarki (szybsze, ale ulotne).
- (from service worker) — zasób został obsłużony przez Service Worker.
- Konkretny rozmiar — zasób został pobrany z sieci.
Dla Speculation Rules API Chrome DevTools oferuje dedykowany panel w zakładce Application → Background Services → Speculative Loads, gdzie można śledzić aktywne prefetche i prerendery. Warto tam zaglądać regularnie.
Nagłówki odpowiedzi jako wskaźniki cache
Dodanie niestandardowych nagłówków pozwala łatwo diagnozować, z której warstwy pochodzi odpowiedź:
# Nagłówek informujący o statusie cache CDN
# Cloudflare automatycznie dodaje:
cf-cache-status: HIT # Odpowiedź z cache Cloudflare
cf-cache-status: MISS # Odpowiedź z serwera origin
cf-cache-status: EXPIRED # Cache wygasł, pobrano nową wersję
# Fastly:
x-cache: HIT
x-cache: MISS
# Niestandardowe nagłówki na serwerze origin:
x-cache-layer: origin
x-cache-layer: varnish
x-response-time: 2ms
Metryki cache do monitorowania
Na poziomie produkcyjnym warto monitorować następujące metryki:
- Cache Hit Ratio — procent żądań obsłużonych z cache. Cel: powyżej 90% dla zasobów statycznych, powyżej 70% dla dynamicznych.
- TTFB w podziale na cache hit/miss — pozwala zmierzyć rzeczywisty wpływ cachowania na czas odpowiedzi.
- Bandwidth Savings — ile transferu oszczędzasz dzięki cachowaniu. Ten wskaźnik potrafi przekonać nawet najbardziej sceptyczne osoby z zarządu.
- Stale Serve Rate — jak często serwujesz nieaktualne odpowiedzi (np. z
stale-while-revalidate).
Kompletna strategia cachowania: rekomendacje na 2026 rok
Na zakończenie podsumujmy wszystko w spójną strategię, którą możesz wdrożyć na swojej stronie. Traktuj to jako checklistę:
Warstwa 1: HTTP Cache-Control
- Zasoby z hashem:
public, max-age=31536000, immutable - Dokumenty HTML:
no-cachez ETag - API publiczne:
public, max-age=60, stale-while-revalidate=300 - Dane prywatne:
private, no-cache
Warstwa 2: Service Worker (Workbox)
- Precaching: shell aplikacji, główny CSS/JS, fonty
- Runtime Cache First: obrazy, ikony
- Runtime Network First: HTML, API
- Runtime Stale-While-Revalidate: zasoby JS/CSS bez fingerprinting
Warstwa 3: CDN Edge Cache
- Full page caching z ISR dla stron statycznych i semi-statycznych
CDN-Cache-Controldla niezależnego sterowania TTL na edge- Automatyczna inwalidacja cache w pipeline CI/CD
Warstwa 4: Speculation Rules API
- Prefetch: wszystkie linki wewnętrzne z
eagerness: "moderate" - Prerender: najpopularniejsze podstrony z
eagerness: "moderate" - Wykluczenia: strony z akcjami (logout, checkout, API)
Podsumowanie
Wielowarstwowe cachowanie to jedna z najskuteczniejszych technik optymalizacji wydajności webowej — i szczerze mówiąc, zbyt często pomijana na rzecz bardziej „efektownych" optymalizacji. Dobrze zaprojektowana strategia łącząca HTTP Cache-Control, Service Workers z Workbox, CDN Edge Cache i Speculation Rules API potrafi zredukować czas ładowania nawet o 80% przy powtórnych wizytach, a nawigację między stronami uczynić praktycznie natychmiastową.
Każda warstwa cache ma inne mocne strony i ograniczenia. HTTP Cache jest prosty i niezawodny, ale ograniczony do deklaratywnych reguł. Service Workers dają pełną kontrolę programistyczną, ale wymagają więcej kodu. CDN Edge Cache minimalizuje latencję geograficzną, ale wymaga strategii inwalidacji. A Speculation Rules API oferuje natychmiastowe przejścia, ale jest (na razie) ograniczone do przeglądarek Chromium.
Moja rada? Wdrażaj te techniki stopniowo. Zacznij od poprawnych nagłówków Cache-Control, potem dodaj Service Worker z Workbox, a na koniec Speculation Rules. Krok po kroku zbudujesz solidny fundament wydajności, który nie tylko poprawi metryki Core Web Vitals, ale przede wszystkim da Twoim użytkownikom szybsze i przyjemniejsze doświadczenie. A o to w tym wszystkim chodzi.