W 2026 roku przepaść wydajnościowa między “zwykłą” stroną WordPress a witryną klasy enterprise wyznacza inteligencja cache’u. Dekadę temu wystarczało podstawowe cachowanie stron. Nowoczesne aplikacje webowe potrzebują wielowarstwowej strategii, która rozumie, co cachować, gdzie to robić i dokładnie kiedy unieważnić dane.
Dziesięć lat temu cachowanie było proste: zapisywałeś stronę jako plik HTML i serwowałeś ją, aż wygasła. W 2026 roku podejście “wszystko albo nic” już nie wystarcza. Nowoczesne strony to byty hybrydowe: częściowo statyczne, częściowo dynamiczne, serwowane globalnie użytkownikom o różnej prędkości łącza i na różnych urządzeniach. Aby osiągnąć wynik 100/100 w Core Web Vitals przy treściach spersonalizowanych, potrzebujesz strategii, która optymalizuje każdą warstwę dostarczania.
Praktyczna architektura jest warstwowa: cache przeglądarki, krawędź CDN, cache pełnych stron, cache fragmentów, object cache i higiena bazy danych współpracują, zamiast ze sobą konkurować.
Jak działa trójwarstwowy edge caching w WordPressie
W 2026 roku serwer źródłowy (origin) prawie nigdy nie widzi bezpośrednio żądania odwiedzającego. Nowoczesne wdrożenia WordPressa stosują trójwarstwową strategię cachowania, która rozprowadza treść po całym świecie.
Warstwa 1: statyczny cache na krawędzi (CDN)
HTML, obrazy, CSS i JavaScript są przechowywane na krawędzi sieci w usługach takich jak Cloudflare, Bunny.net czy Fastly. To daje TTFB (Time to First Byte) poniżej 50 ms, którego oczekują użytkownicy.
Kluczowe ustawienia konfiguracji:
- Cache Everything: cachuj HTML na krawędzi, nie tylko zasoby statyczne
- Edge TTL: ustaw odpowiedni czas życia cache’u (1 godzina dla treści dynamicznych, 1 rok dla statycznych)
- Browser TTL: steruj nagłówkami cachowania po stronie klienta
- Always Online: serwuj przeterminowaną treść, gdy origin jest niedostępny
Przykład reguł Page Rules w Cloudflare:
*example.com/*
Cache Level: Cache Everything
Edge Cache TTL: 2 hours
Browser Cache TTL: 30 minutes
Always Online: OnWarstwa 2: cachowanie odpowiedzi API
W konfiguracjach headless lub decoupled WordPress odpowiedzi API są cachowane osobno od HTML:
- endpointy REST API cachowane z krótkim TTL (5-15 minut)
- zapytania GraphQL cachowane według złożoności i zmienności danych
- czyszczenie cache’u wyzwalane webhookami dla natychmiastowych aktualizacji
Korzyści:
- mniejsze obciążenie bazy danych przy frontendach opartych na JavaScripcie
- szybsze działanie aplikacji mobilnych
- obsługa funkcji czasu rzeczywistego z zapasową wersją z cache’u
Warstwa 3: cache locking i ochrona przed thundering herd
Gdy element w cache’u wygasa, tradycyjne konfiguracje przepuszczają wiele równoczesnych żądań do serwera źródłowego. W 2026 roku wdrażamy cache locking:
- tylko pierwsze żądanie odbudowuje treść w cache’u
- kolejne żądania dostają lekko przeterminowaną wersję
- po zakończeniu odbudowy wszyscy użytkownicy dostają świeżą treść
Implementacja w Cloudflare Workers:
// Cache locking pattern
async function handleRequest(request) {
const cache = caches.default;
const cacheKey = new Request(request.url);
let response = await cache.match(cacheKey);
if (!response) {
// Check for regeneration lock
const lockKey = `lock:${request.url}`;
const lock = await CACHE_LOCKS.get(lockKey);
if (lock) {
// Serve stale while regenerating
response = await cache.match(`${cacheKey}:stale`);
} else {
// Acquire lock and regenerate
await CACHE_LOCKS.put(lockKey, 'locked', {expirationTtl: 30});
response = await fetchOrigin(request);
await cache.put(cacheKey, response.clone());
await cache.put(`${cacheKey}:stale`, response.clone());
}
}
return response;
}Cache pełnych stron z Nginx FastCGI i Varnish
Sieci brzegowe szybko dostarczają zasoby statyczne i publiczny HTML, ale origin nadal potrzebuje wydajnego cache’u pełnych stron, zanim w ogóle ruszy PHP. Nginx FastCGI cache przechowuje wyrenderowane odpowiedzi HTML w strefach pamięci współdzielonej i na dysku lokalnym, więc anonimowi odwiedzający w ogóle nie angażują workerów PHP-FPM:
# FastCGI cache configuration in nginx.conf
fastcgi_cache_path /var/run/nginx-cache levels=1:2 keys_zone=WORDPRESS:256m inactive=1d max_size=4g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
# Purge location using fastcgi_cache_purge
location ~ /purge(/.*) {
allow 127.0.0.1;
allow 10.0.0.0/8;
deny all;
fastcgi_cache_purge WORDPRESS "$scheme$request_method$host$1";
}Gdy redaktor aktualizuje treść, wtyczka do czyszczenia wysyła żądanie HTTP PURGE do lokalnego endpointu Nginx i wpis dla danego klucza cache’u zostaje usunięty. Dokumentacja modułu FastCGI w nginx podaje, że nazwy plików cache’u to skrót MD5 klucza, a dyrektywę fastcgi_cache_purge opisuje jako część subskrypcji komercyjnej. Otwartoźródłowe buildy Nginx korzystają więc do tego samego z zewnętrznego modułu ngx_cache_purge. Ta sama strona opisuje dwie dyrektywy, które warto włączyć na każdym obciążonym originie WordPressa: fastcgi_cache_lock, która pozwala tylko jednemu żądaniu naraz wypełniać nowy element cache’u, oraz fastcgi_cache_use_stale, która serwuje przeterminowaną odpowiedź, gdy backend FastCGI zwraca błąd. W środowiskach wieloserwerowych z Varnishem żądania PURGE lub wyrażenia BAN czyszczą wpisy według wzorca URL albo według nagłówków z tagami wysyłanych przez wtyczki WordPressa.
Jak czyścić cache WordPressa po tagach
Największym problemem cachowania w dużych serwisach zawsze był “problem czyszczenia”. Poprawiasz literówkę na stronie głównej, a cache całej witryny zostaje usunięty, co powoduje gwałtowny spadek wydajności i przeciążenie serwera źródłowego.
W 2026 roku rozwiązujemy to przez cache tagging.
Jak działa tagowanie cache’u
Zamiast traktować zcachowaną treść jak odizolowane pliki, oznaczamy każdy element cache’u metadanymi o jego zależnościach:
Homepage: tags=["home", "post_123", "post_124", "category_news"]
Blog Post: tags=["post_125", "category_tutorials", "author_john"]
Category Page: tags=["category_tutorials", "post_125", "post_126"]Gdy aktualizujesz “Post A”, czyszczone są tylko elementy oznaczone tagiem post_a:
- sam wpis
- archiwa jego kategorii
- widżety powiązanych wpisów
- archiwa autora
- strona główna (jeśli wpis jest na niej wyróżniony)
Reszta z 10 000 stron pozostaje w cache’u i działa szybko.
Jak wdrożyć cache tags w WordPressie
Z Cloudflare:
// Add cache tags to Cloudflare
function add_cache_tags_header() {
if (is_single()) {
$tags = [
'post_' . get_the_ID(),
'author_' . get_the_author_meta('ID'),
];
// Add category tags
$categories = get_the_category();
foreach ($categories as $cat) {
$tags[] = 'category_' . $cat->slug;
}
header('Cache-Tag: ' . implode(',', $tags));
}
}
add_action('template_redirect', 'add_cache_tags_header');Czyszczenie po tagu:
// Purge specific tags when post is updated
function purge_post_cache_tags($post_id) {
$tags = [
'post_' . $post_id,
'author_' . get_post_field('post_author', $post_id),
];
// Cloudflare API purge by tag
$api = new Cloudflare\API\Client($email, $api_key);
$api->zones()->cachePurgeTags($zone_id, $tags);
}
add_action('save_post', 'purge_post_cache_tags');Które tagi cache’u czyścić po zmianie wpisu
Hierarchia tagów:
post_{id}: treść pojedynczego wpisucategory_{slug}: archiwa kategoriiauthor_{id}: archiwa autorataxonomy_{name}_{term}: strony własnych taksonomiiwidget_{id}: treść dynamicznych widżetówglobal: elementy całej witryny (nagłówek, stopka, menu)
Czyszczenie warunkowe:
// Only purge what changed
function smart_cache_purge($post_id) {
$post = get_post($post_id);
$tags = ['post_' . $post_id];
// If featured image changed, purge image-related caches
if (did_post_thumbnail_change($post_id)) {
$tags[] = 'featured_images';
}
// If post status changed, purge archive listings
if ($post->post_status !== get_previous_post_status($post_id)) {
$tags[] = 'category_' . get_primary_category($post_id);
$tags[] = 'author_' . $post->post_author;
}
purge_cache_tags($tags);
}Redis jako object cache dla WordPressa w 2026 roku
Baza danych to wąskie gardło WordPressa. Każde wczytanie strony uruchamia dziesiątki zapytań SQL po wpisy, metadane, opcje i dane użytkowników. Object caching przechowuje wyniki tych zapytań w pamięci RAM i eliminuje powtarzane odczyty z bazy.
Dlaczego Redis 8+ w 2026 roku
Informacje o wydaniu Redis Open Source 8.0 (maj 2025) mówią, że Redis Search, JSON, szeregi czasowe i pięć probabilistycznych struktur danych, wcześniej osobne moduły, są teraz częścią samego Redisa. Dla WordPressa oznacza to, że jeden serwer może trzymać object cache i jednocześnie dane używane przez wyszukiwarkę lub metryki:
Funkcje Redis 8:
- RedisJSON: przechowywanie i odpytywanie złożonych obiektów JSON
- RedisSearch: wyszukiwanie pełnotekstowe w danych z cache’u
- RedisTimeSeries: śledzenie metryk wydajności cache’u
- Opcje trwałości: RDB i AOF dla trwałości danych
Redis czy Memcached (porównanie 2026):
| Cecha | Redis 8+ | Memcached |
|---|---|---|
| Typy danych | 8+ (strings, hashes, lists, sets, sorted sets, JSON, streams) | 1 (strings) |
| Trwałość | RDB + AOF | Brak |
| Replikacja | Master-slave + Cluster | Brak |
| Wydajność pamięci | Lepsza z kompresją | Dobra |
| Unieważnianie po tagach | Ręczne (np. zbiory kluczy) | Ręczna implementacja |
| Możliwości zapytań | RedisSearch | Brak |
Drop-in Redisa a Memcached i mechanika object cache
Rdzeń WordPressa udostępnia wymienny interfejs cachowania przez plik wp-content/object-cache.php. Bez drop-ina object cache WordPress używa cache’u w pamięci, który żyje tylko przez czas jednego żądania PHP. Z trwałym drop-inem (na przykład tym instalowanym przez wtyczkę Redis Object Cache albo drop-inem Memcached) wywołania wp_cache_set(), wp_cache_get(), wp_cache_delete() i wp_cache_add() obsługuje serwer Redis lub Memcached, a dane przetrwają między żądaniami. Dokumentacja klasy WP_Object_Cache stwierdza wprost, że domyślnie object cache nie jest trwały: dane siedzą wyłącznie w pamięci i tylko na czas trwania żądania.
Programiści często mylą transienty (set_transient(), get_transient()) z bezpośrednimi wywołaniami object cache (wp_cache_set(), wp_cache_get()):
- Gdy aktywny jest zewnętrzny drop-in object cache,
set_transient()wywołuje bezpośredniowp_cache_set()w grupietransienti całkowicie omija tabelęwp_options. - Bez trwałego drop-ina transienty trafiają do
wp_optionsjako wiersze (_transient_*i_transient_timeout_*). W kodzieset_transient()w rdzeniu transient zapisany z czasem wygaśnięcia dostaje wiersz timeout i nie jest ładowany automatycznie, a transient bez czasu wygaśnięcia jest autoloadowany przy każdym żądaniu. Transienty bez terminu po cichu powiększają więc autoloadowane opcje. - Podręcznik Transients API ogranicza klucz do 172 znaków i cytuje Ryana McCue: “czasy wygaśnięcia transientów są czasem maksymalnym. Nie ma minimalnego wieku” (tłumaczenie własne). Kod musi zawsze obsłużyć brak wartości, bo Redis lub Memcached mogą usunąć ją przed upływem TTL.
- Bezpośrednie wywołania
wp_cache_*pozwalają na własne grupy cache’u i precyzyjne unieważnianie.
// Storing complex computed data in a dedicated cache group
$cache_key = 'user_order_summary_' . $user_id;
$order_summary = wp_cache_get($cache_key, 'orders_analytics');
if (false === $order_summary) {
$order_summary = compute_heavy_user_order_metrics($user_id);
// Cache for 15 minutes in the orders_analytics group
wp_cache_set($cache_key, $order_summary, 'orders_analytics', 900);
}Grupy cache’u i grupy nietrwałe
Domyślnie zewnętrzny object cache utrwala wszystkie grupy między żądaniami HTTP. Dane bardzo zmienne albo związane z jednym żądaniem nie powinny jednak zajmować zewnętrznej pamięci RAM ani kosztować czasu na komunikację z serwerem. Rdzeń WordPressa udostępnia wp_cache_add_non_persistent_groups(), aby wybrane grupy trzymać wyłącznie w pamięci procesu PHP:
// Register non-persistent groups in an early hook or mu-plugin
function register_custom_cache_groups() {
// Keep temporary session flags in PHP process RAM only
wp_cache_add_non_persistent_groups([
'runtime_request_flags',
'transient_session_tokens',
]);
}
add_action('init', 'register_custom_cache_groups');W sieciach multisite wp_cache_add_global_groups() sprawia, że klucze w trwałych grupach są współdzielone przez wszystkie witryny sieci, zamiast dostawać prefiksy konkretnej witryny.
Implementacja object cache w WordPressie
Podstawowa konfiguracja Redisa (wp-config.php):
// Enable object cache
define('WP_CACHE', true);
// Redis settings
define('WP_REDIS_HOST', '127.0.0.1');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_DATABASE', 0);
define('WP_REDIS_TIMEOUT', 1);
define('WP_REDIS_READ_TIMEOUT', 1);
// Advanced settings
define('WP_REDIS_DISABLE_METRICS', false);
define('WP_REDIS_DISABLE_BANNERS', true);
define('WP_REDIS_PREFIX', 'wp_');Strategia micro-cachingu: Nawet krótkotrwałe wpisy w cache’u, na przykład lista najnowszych komentarzy trzymana przez 30-60 sekund, wyraźnie odciążają bazę:
// Cache recent comments for 30 seconds
function get_cached_recent_comments($limit = 5) {
$cache_key = 'recent_comments_' . $limit;
$comments = wp_cache_get($cache_key);
if (false === $comments) {
$comments = get_comments([
'number' => $limit,
'status' => 'approve',
]);
wp_cache_set($cache_key, $comments, '', 30); // 30 seconds
}
return $comments;
}Relacyjny object cache: Nowoczesne implementacje rozumieją zależności między obiektami, więc zmiana obiektu nadrzędnego czyści też elementy od niego zależne:
// Automatic relationship caching
class RelationalObjectCache {
public function get_post_with_meta($post_id) {
$cache_key = "post_with_meta:{$post_id}";
$data = wp_cache_get($cache_key);
if (false === $data) {
$post = get_post($post_id);
$meta = get_post_meta($post_id);
$author = get_userdata($post->post_author);
$data = [
'post' => $post,
'meta' => $meta,
'author' => $author,
];
// Cache with relationship tags
wp_cache_set($cache_key, $data, '', 3600);
wp_cache_add_relationship($cache_key, "post:{$post_id}");
wp_cache_add_relationship($cache_key, "user:{$post->post_author}");
}
return $data;
}
}Monitorowanie i optymalizacja Redisa
Monitorowanie współczynnika trafień cache’u:
# Redis CLI commands for monitoring
redis-cli info stats | grep keyspace
redis-cli info memory
redis-cli slowlog get 10Integracja z panelem WordPressa:
// Add cache stats to admin bar
function add_cache_stats_to_admin_bar($wp_admin_bar) {
$stats = wp_cache_get_stats();
$hit_rate = ($stats['hits'] / ($stats['hits'] + $stats['misses'])) * 100;
$wp_admin_bar->add_node([
'id' => 'cache_stats',
'title' => sprintf('Cache: %.1f%% hit rate', $hit_rate),
'href' => admin_url('tools.php?page=redis-cache'),
]);
}Jak cachować dynamiczne treści WordPressa przez ESI
Jak cachować stronę, która wita użytkownika zwrotem “Witaj, [Imię]” albo pokazuje sumę koszyka? W 2026 roku łączymy treść statyczną i dynamiczną przez Edge Side Includes (ESI) lub hydratację po stronie klienta.
Dlaczego cache pełnych stron psuje personalizację
Tradycyjny cache pełnych stron psuje personalizację:
- powitania stają się statyczne (“Witaj, Gościu” dla wszystkich)
- koszyki pokazują nieaktualny albo pusty stan
- pasek administratora pojawia się u wszystkich użytkowników
- dynamiczne ceny się nie aktualizują
Jak cache fragmentów wypełnia placeholdery na krawędzi
Główna strona jest w 100% zcachowana jako statyczny HTML. Elementy dynamiczne to placeholdery wypełniane na krawędzi sieci albo przez JavaScript:
Implementacja ESI:
<!-- Static cached content -->
<h1>Welcome to Our Store</h1>
<!-- Dynamic fragment -->
<esi:include src="/esi/cart-count.php" />
<!-- Static content continues -->
<p>Check out our latest products...</p>
<!-- Personalized greeting -->
<esi:include src="/esi/user-greeting.php" />Hydratacja po stronie klienta (alternatywa):
<!-- Static placeholder -->
<div id="cart-count" data-dynamic-url="/api/cart-count">
<span class="placeholder">--</span>
</div>
<script>
// Hydrate on page load
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-dynamic-url]').forEach(el => {
fetch(el.dataset.dynamicUrl)
.then(r => r.text())
.then(html => el.innerHTML = html);
});
});
</script>Implementacja cache’u fragmentów w WordPressie
Varnish ESI z WordPressem:
// Mark dynamic content with ESI tags
function esi_dynamic_fragment($content, $callback, $args = []) {
$esi_url = add_query_arg([
'esi_action' => $callback,
'esi_args' => base64_encode(json_encode($args)),
], site_url('/esi-endpoint/'));
return "<esi:include src=\"{$esi_url}\" />";
}
// Usage in templates
echo esi_dynamic_fragment('user_greeting', ['user_id' => get_current_user_id()]);Pominięcie cache’u fragmentów koszyka i checkoutu w WooCommerce:
W standardowych sklepach WooCommerce żądanie wc-ajax=get_refreshed_fragments uruchamia się przy każdym niezcachowanym wczytaniu strony, aby zaktualizować sumę i liczbę produktów w koszyku. Każde takie żądanie uruchamia PHP na originie. Wyłączenie skryptu fragmentów na stronach, które nie pokazują koszyka, i zastąpienie go przez ESI albo pamięć po stronie klienta tam, gdzie koszyk jest widoczny, usuwa to żądanie:
// Dequeue cart fragments on non-shop and static content pages
function bypass_woocommerce_cart_fragments() {
if (is_front_page() || is_single() || is_page()) {
if (!is_woocommerce() && !is_cart() && !is_checkout()) {
wp_dequeue_script('wc-cart-fragments');
}
}
}
add_action('wp_enqueue_scripts', 'bypass_woocommerce_cart_fragments', 20);Na stronach checkoutu, koszyka i konta cache stron trzeba całkowicie pominąć zarówno na krawędzi, jak i w Nginx, na podstawie ciasteczek:
- Gdy istnieje ciasteczko
woocommerce_items_in_cart, cache na krawędzi pomija wyszukiwanie w cache’u pełnych stron i przekazuje żądanie prosto do originu. - Wrażliwe sesje (
woocommerce_cart_hash,wp_woocommerce_session_) od razu dostają nagłówki odpowiedziCache-Control: private, no-cache, no-store, must-revalidate. - Fragmenty koszyka na zcachowanych stronach katalogu odświeża się z
localStorageisessionStorageprzeglądarki, aktualizując DOM mini-koszyka bez uruchamiania PHP na originie.
Fragmenty koszyka i wykluczenia checkoutu to część pracy nad opóźnieniami sklepu, nie tylko konfiguracja ESI: zobacz optymalizację wydajności WooCommerce 2026.
Jak używać Speculation Rules API do prefetchingu w WordPressie
W 2026 roku cachowanie dotyczy nie tylko tego, co już się wydarzyło, ale też tego, co się wydarzy. Dzięki Speculation Rules API witryna WordPress może przewidzieć i wczytać treść, zanim użytkownik kliknie.
Jak działa spekulatywny prefetching
O tym, kiedy przeglądarka działa, decyduje ustawienie eagerness. Według dokumentacji Chrome o prerenderingu na komputerach eager spekuluje po 10 milisekundach trzymania kursora nad linkiem, moderate po 200 milisekundach (albo wcześniej, przy pointerdown), a conservative dopiero przy wciśnięciu przycisku lub dotknięciu. Przy trybie prerender przeglądarka renderuje docelową stronę w tle, więc kolejne kliknięcie działa natychmiast.
WordPress 6.8 ma to w rdzeniu. Notatka deweloperska o speculative loading podaje, że ustawienie domyślne w praktyce oznacza tryb prefetch i eagerness conservative, że funkcja jest wyłączona dla zalogowanych użytkowników i na stronach bez ładnych bezpośrednich odnośników (pretty permalinks), a adresy z parametrami zapytania są wykluczone. Filtr wp_speculation_rules_configuration zmienia tryb i eagerness, a wp_speculation_rules_href_exclude_paths dodaje wykluczenia, na przykład ścieżki koszyka i checkoutu.
Implementacja:
<script type="speculationrules">
{
"prerender": [{
"source": "list",
"urls": [
"/about/",
"/services/",
"/contact/"
]
}, {
"source": "document",
"where": {
"href_matches": "/blog/*",
"selector_matches": "a[rel='prerender']"
}
}]
}
</script>Jak dodać speculation rules w WordPressie przez PHP
Prefetching oparty na treści:
// Add speculation rules based on current page
function add_speculation_rules() {
if (!is_singular('post')) {
return;
}
// Get related posts for prefetching
$related = get_related_posts(get_the_ID(), 3);
$urls = array_map(function($post) {
return get_permalink($post->ID);
}, $related);
// Add next/prev posts
if ($next = get_next_post()) {
$urls[] = get_permalink($next->ID);
}
$rules = [
'prerender' => [[
'source' => 'list',
'urls' => $urls,
]],
];
echo '<script type="speculationrules">' .
json_encode($rules) .
'</script>';
}
add_action('wp_head', 'add_speculation_rules');Prefetching po najechaniu kursorem (wariant zapasowy):
// Prefetch on link hover
let prefetchTimeout;
document.addEventListener('mouseover', (e) => {
const link = e.target.closest('a');
if (!link || link.hostname !== location.hostname) return;
prefetchTimeout = setTimeout(() => {
const prefetchLink = document.createElement('link');
prefetchLink.rel = 'prefetch';
prefetchLink.href = link.href;
document.head.appendChild(prefetchLink);
}, 100);
});
document.addEventListener('mouseout', () => {
clearTimeout(prefetchTimeout);
});Nagłówki stale-while-revalidate dla WordPressa
Użytkownicy nie znoszą kręcących się wskaźników ładowania. SWR pozwala przeglądarce od razu podać “przeterminowaną” (nieco starszą) treść i w tle pobrać świeżą wersję.
Jak działa SWR
- Użytkownik żąda strony
- Przeglądarka sprawdza cache
- Jeśli treść jest w cache’u (nawet przeterminowana), zostaje podana od razu
- W tle przeglądarka pobiera świeżą treść z originu
- Cache zostaje zaktualizowany, a treść strony opcjonalnie odświeżona
Nagłówek Cache-Control:
Cache-Control: max-age=60, stale-while-revalidate=300Oznacza to, że:
- treść jest świeża przez 60 sekund
- po 60 sekundach przeterminowana treść jest podawana jeszcze do 300 sekund, a w tym czasie trwa rewalidacja
- po łącznie 360 sekundach wymagana jest świeża treść
Jak wdrożyć SWR w WordPressie
Implementacja przez Service Worker:
// service-worker.js
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
// Return cached version immediately (even if stale)
const fetchPromise = fetch(event.request).then((networkResponse) => {
caches.open('dynamic').then((cache) => {
cache.put(event.request, networkResponse.clone());
});
return networkResponse;
});
// Return stale or wait for fresh
return response || fetchPromise;
})
);
});Konfiguracja SWR dla Cloudflare:
// Add SWR headers to dynamic content
function add_swr_headers($headers) {
if (is_singular() && !is_user_logged_in()) {
$headers['Cache-Control'] = 'public, max-age=60, stale-while-revalidate=300';
}
return $headers;
}
add_filter('wp_headers', 'add_swr_headers');Jakich dyrektyw Cache-Control potrzebują strony WordPressa na krawędzi
Stale-while-revalidate to tylko jedna z kilku dyrektyw, a pozostałe decydują, czy strona w ogóle może trafić do cache’u. Opis nagłówka Cache-Control w MDN definiuje te, które mają znaczenie dla WordPressa:
| Dyrektywa | Definicja MDN (w skrócie) | Zastosowanie w WordPressie |
|---|---|---|
s-maxage | Czas świeżości we współdzielonym cache’u; tam nadpisuje max-age i Expires | Długi TTL na krawędzi dla anonimowego HTML, podczas gdy przeglądarki dostają krótkie max-age |
stale-while-revalidate | Cache może ponownie użyć przeterminowanej odpowiedzi podczas rewalidacji | Archiwa i artykuły zmieniane kilka razy dziennie |
stale-if-error | Cache może użyć przeterminowanej odpowiedzi, gdy upstream zwraca 500, 502, 503 lub 504 | Strony działają podczas awarii PHP-FPM lub bazy danych |
private | Odpowiedź może zapisać tylko cache prywatny, na przykład przeglądarka | Strony konta, widoki dla zalogowanych |
no-store | Żaden cache nie może zapisać odpowiedzi | Koszyk, checkout, reset hasła |
Dwa szczegóły z dokumentacji Cloudflare o cache-control zmieniają sposób, w jaki te dyrektywy się łączą. Po pierwsze, Cloudflare traktuje s-maxage jako regułę, że współdzielony cache musi zrewalidować odpowiedź z originem, zanim poda przeterminowaną treść, więc tam s-maxage wyłącza stale-while-revalidate. Jeśli chcesz rewalidacji w tle na krawędzi Cloudflare, wysyłaj max-age razem z stale-while-revalidate, a TTL na krawędzi ustaw regułą Cache Rule. Po drugie, przy włączonym Origin Cache Control i domyślnym poziomie cache’u odpowiedź z nagłówkiem Set-Cookie nie jest cachowana. WordPress ustawia ciasteczka zalogowanym użytkownikom, a WooCommerce koszykom, więc przypadkowy Set-Cookie na publicznej stronie po cichu wyłącza jej cache na krawędzi.
Ważny jest też punkt wyjścia. Strona Cloudflare o domyślnym zachowaniu cache’u podaje, że CDN Cloudflare domyślnie nie cachuje HTML ani JSON. Cachuje według rozszerzenia pliku, a nie typu MIME. Cache pełnych stron WordPressa na krawędzi zawsze wymaga więc jawnej reguły Cache Rule, która dopuszcza HTML, z wyjątkami dla wp-admin, wp-login.php, REST API tam, gdzie zwraca dane prywatne, oraz ciasteczek wordpress_logged_in_ i woocommerce_items_in_cart.
Jak monitorować i debugować cache WordPressa
Skuteczne cachowanie wymaga wglądu w to, co się dzieje. W 2026 roku do pilnowania skuteczności cache’u używamy narzędzi monitorujących, na przykład nagłówków X-Cache-Status, które pokazują, czy współczynnik trafień jest właściwy.
Jak dodać nagłówki debugowania X-Cache-Status
Dodaj nagłówki debugowania, aby śledzić zachowanie cache’u:
function add_cache_debug_headers() {
if (!current_user_can('manage_options')) {
return;
}
header('X-Cache-Status: ' . (did_cache_hit() ? 'HIT' : 'MISS'));
header('X-Cache-TTL: ' . get_cache_remaining_ttl());
header('X-Cache-Tags: ' . implode(', ', get_current_page_cache_tags()));
}
add_action('template_redirect', 'add_cache_debug_headers');Jak logować trafienia i chybienia cache’u w Redis TimeSeries
// Log cache metrics to Redis TimeSeries
function log_cache_metric($metric, $value) {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// Add to time series
$redis->rawCommand('TS.ADD',
"cache:{$metric}",
'*',
$value,
'LABELS', 'site', get_site_url(), 'type', 'wordpress'
);
}
// Log on every request
add_action('wp', function() {
log_cache_metric('requests', 1);
if (did_cache_hit()) {
log_cache_metric('hits', 1);
} else {
log_cache_metric('misses', 1);
}
});Panel monitorowania cache’u w kokpicie WordPressa
Utwórz w panelu WordPressa widok do monitorowania cache’u:
// Add cache health page
function add_cache_health_page() {
add_management_page(
'Cache Health',
'Cache Health',
'manage_options',
'cache-health',
'render_cache_health_page'
);
}
add_action('admin_menu', 'add_cache_health_page');
function render_cache_health_page() {
$stats = wp_cache_get_stats();
$hit_rate = $stats['hits'] / ($stats['hits'] + $stats['misses']) * 100;
?>
<div class="wrap">
<h1>Cache Health Dashboard</h1>
<div class="cache-stats">
<div class="stat-box">
<h3>Hit Rate</h3>
<div class="stat-value <?php echo $hit_rate > 90 ? 'good' : 'warning'; ?>">
<?php echo number_format($hit_rate, 1); ?>%
</div>
</div>
<div class="stat-box">
<h3>Cached Objects</h3>
<div class="stat-value"><?php echo number_format($stats['objects']); ?></div>
</div>
<div class="stat-box">
<h3>Memory Used</h3>
<div class="stat-value"><?php echo size_format($stats['memory']); ?></div>
</div>
</div>
</div>
<?php
}Jak Site Health w WordPressie wykrywa cache stron i object cache
Od WordPressa 6.1 ekran Narzędzia, Kondycja witryny (Site Health) uruchamia dwa testy, które sprawdzają cachowanie od środka, a ich reguły są jawne w kodzie rdzenia. Ich lektura wyjaśnia, dlaczego strona z działającym CDN wciąż może widzieć ostrzeżenie.
Test cache’u stron wysyła trzy żądania do strony głównej, zapisuje nagłówki odpowiedzi i bierze medianę czasu odpowiedzi. Odpowiedź uznaje się za zcachowaną, gdy ma jeden z nagłówków z listy w get_page_cache_headers(): cache-control z niezerowym max-age, expires z datą w przyszłości, dodatnie age, last-modified, etag albo via, a także nagłówki dostawców, takie jak cf-cache-status, x-cache, x-cache-status, x-litespeed-cache, x-srcache-fetch-status z wartością HIT czy x-varnish z dwoma identyfikatorami żądań. Wtyczka cache’u stron zostaje wykryta tylko wtedy, gdy w kodzie rdzenia spełnione są trzy warunki: istnieje plik wp-content/advanced-cache.php, stała WP_CACHE ma wartość true, a filtr enable_loading_advanced_cache_dropin nie został wyłączony. Te same trzy warunki decydują, czy wp-settings.php w ogóle wczyta advanced-cache.php, bardzo wcześnie, jeszcze przed wtyczkami.
Próg czasu odpowiedzi wynosi domyślnie 600 milisekund. Funkcja get_good_response_time_threshold() w rdzeniu dokumentuje, że wartość opiera się na wytycznych web.dev dotyczących time to first byte, a zmienia ją filtr site_status_good_response_time_threshold. Strona za Cloudflare, której origin odpowiada w 900 ms, bo test omija krawędź albo HTML nie jest cachowany na krawędzi, zostanie oznaczona, nawet jeśli odwiedzający dostają szybkie odpowiedzi.
Test trwałego object cache zaleca Redisa lub Memcached, gdy spełniony jest którykolwiek z warunków:
- witryna jest siecią multisite (zalecenie zawsze),
- ponad 500 autoloadowanych opcji albo ponad 100 000 bajtów zserializowanych autoloadowanych opcji,
- 1000 lub więcej wierszy w tabeli komentarzy, opcji, wpisów, terminów lub użytkowników (rdzeń czyta szacunki wierszy InnoDB z
information_schema, a nie dokładne liczby).
Progi zmienia filtr site_status_persistent_object_cache_thresholds, a site_status_should_suggest_persistent_object_cache pomija całe sprawdzenie. Hostingi, które zapewniają object cache w innej warstwie, korzystają z tego filtra, więc brak ostrzeżenia nie dowodzi, że drop-in jest zainstalowany: sprawdź, czy istnieje wp-content/object-cache.php, albo wywołaj wp_using_ext_object_cache().
Najczęstsze błędy cachowania w WordPressie i jak je naprawić
Jak uniknąć zbyt agresywnego cachowania
Problem: cachowanie wszystkiego prowadzi do nieaktualnych treści i frustracji użytkowników.
Rozwiązanie: wprowadź przemyślane wykluczenia z cache’u:
function should_cache_page() {
// Don't cache for logged-in users
if (is_user_logged_in()) {
return false;
}
// Don't cache dynamic forms
if (is_page('contact') || is_page('checkout')) {
return false;
}
// Don't cache search results (too many variations)
if (is_search()) {
return false;
}
return true;
}Jak zapobiec cache stampede
Problem: gdy wygasa popularny element cache’u, setki równoczesnych żądań jednocześnie trafiają na brak w cache’u. Każdy proces uruchamia to samo ciężkie zapytanie SQL albo zdalne wywołanie API wobec bazy na originie, co powoduje skoki CPU, rywalizację o blokady i awarie serwera (dogpiling, czyli thundering herd).
Rozwiązanie: zastosuj probabilistyczne wcześniejsze wygaszanie algorytmem XFetch albo rozproszone blokady mutex w Redisie:
Probabilistyczne wcześniejsze wygaszanie algorytmem XFetch
To podejście w ogóle unika twardych blokad. Algorytm XFetch, zaproponowany przez Vattaniego, Chierichettiego i Lowensteina, wylicza prawdopodobieństwo wcześniejszego odświeżenia, które rośnie wraz ze zbliżaniem się wygaśnięcia i z czasem potrzebnym na obliczenie wartości. Jeden worker odświeża element z wyprzedzeniem, a wszystkie przychodzące żądania nadal dostają ważne dane z cache’u:
/**
* Probabilistic early cache recomputation (XFetch algorithm).
*
* @param string $key Cache identifier
* @param callable $compute Callback generating fresh data
* @param int $ttl Cache TTL in seconds
* @param float $beta Aggressiveness multiplier (> 0, default 1.0)
* @return mixed Cached or computed payload
*/
function xfetch_cache_get($key, callable $compute, $ttl = 3600, $beta = 1.0) {
$cached = wp_cache_get($key, 'xfetch_group');
// Expected format: ['data' => ..., 'delta' => compute_time_seconds, 'expiry' => timestamp]
if (is_array($cached) && isset($cached['data'], $cached['delta'], $cached['expiry'])) {
$remaining = $cached['expiry'] - time();
// Compute probabilistic trigger: -beta * delta * ln(random_float(0, 1))
$random = mt_rand(1, 1000000) / 1000000;
$early_trigger = -($beta * $cached['delta'] * log($random));
// If remaining lifetime exceeds early trigger threshold, return cached value immediately
if ($remaining > $early_trigger) {
return $cached['data'];
}
}
// Refresh cache item
$start = microtime(true);
$fresh_data = $compute();
$delta = microtime(true) - $start;
$payload = [
'data' => $fresh_data,
'delta' => max($delta, 0.001),
'expiry' => time() + $ttl,
];
wp_cache_set($key, $payload, 'xfetch_group', $ttl * 2);
return $fresh_data;
}Rozproszona blokada mutex w Redisie
Gdy obliczanie w tle nie wchodzi w grę, rozproszona blokada mutex gwarantuje, że kosztowne zapytanie przelicza dokładnie jeden proces PHP-FPM:
function get_cached_with_distributed_lock($key, callable $compute, $ttl = 3600) {
$data = wp_cache_get($key, 'distributed_cache');
if (false !== $data) {
return $data;
}
$lock_key = 'lock:' . $key;
// Atomic SET lock:key token NX EX 10
$lock_acquired = wp_cache_add($lock_key, 1, 'distributed_locks', 10);
if ($lock_acquired) {
try {
$fresh_data = $compute();
wp_cache_set($key, $fresh_data, 'distributed_cache', $ttl);
return $fresh_data;
} finally {
wp_cache_delete($lock_key, 'distributed_locks');
}
}
// If another process holds the lock, wait briefly or serve fallback data
usleep(80000); // 80ms sleep
$data = wp_cache_get($key, 'distributed_cache');
return (false !== $data) ? $data : $compute();
}Jak różnicować cache dla urządzeń mobilnych
Problem: użytkownicy desktopowi i mobilni dostają tę samą wersję z cache’u, co psuje responsywny układ.
Rozwiązanie: różnicuj cache według typu urządzenia:
// Add device type to cache key
function get_device_cache_key($base_key) {
$device = wp_is_mobile() ? 'mobile' : 'desktop';
return "{$base_key}_{$device}";
}
// Or use Vary header for CDN
function add_device_vary_header($headers) {
$headers['Vary'] = 'User-Agent';
return $headers;
}Podsumowanie
Cachowanie w 2026 roku nie polega już na oszczędzaniu miejsca, tylko czasu. Inteligentna strategia cachowania to różnica między witryną WordPress klasy enterprise a spóźnioną konkurencją.
Opanowując edge caching, cache tags, cache fragmentów i wzorce SWR, zmieniasz swojego WordPressa w rozproszony, wydajny silnik, który obsługuje miliony żądań z czasem odpowiedzi poniżej 50 ms.
Najważniejsze wnioski:
- Wdróż trójwarstwowy edge caching dla wydajności na całym świecie
- Używaj tagowania cache’u do precyzyjnego unieważniania
- Wdróż Redis 8+ jako object cache dla dużych serwisów
- Łącz treść statyczną i dynamiczną przez cache fragmentów
- Stosuj SWR, aby strony działały natychmiast
- Stale monitoruj kondycję cache’u
- Testuj dokładnie: błędy zapisane w cache’u trudniej znaleźć
Czy cache Twojej strony pracuje dla Ciebie, czy przeciwko Tobie? Napisz do WPPoland, aby przeprowadzić audyt i rozbudować architekturę cachowania. Zobacz też nasze usługi przyspieszania WordPressa.
Powiązane materiały
- Bezpieczeństwo, analityka i optymalizacja WordPressa: szerszy przegląd optymalizacji wydajności
- Optymalizacja bazy danych WordPress: zapytania, autoload i indeksy
- Definitywny przewodnik optymalizacji WooCommerce, edycja 2026: strategie cachowania dla e-commerce







