Cache to najpotężniejsze narzędzie wydajnościowe w Laravelu — i jednocześnie najczęstsze źródło subtelnych błędów. „Dodałem cache i strona przyspieszyła" bywa prawdą przez tydzień, po którym użytkownicy zgłaszają nieaktualne dane, a zespół wyłącza cache w panice. Ten poradnik pokazuje strategie, które działają długoterminowo: co cache'ować, jak unieważniać i jak nie strzelić sobie w stopę.
Najważniejsze: Najważniejsze — cache'uj dane drogie do policzenia i rzadko zmieniane (agregaty, konfiguracje, drzewa kategorii), nie całe aplikacje „na wszelki wypadek". Unieważniaj przez tagi (Redis) albo wersjonowanie kluczy, nie przez
Cache::flush(). Do widoków użyj@cachew Blade, do całych odpowiedzi — response cache z unieważnianiem po zapisie modelu.
Po co cache — trzy warstwy, które warto rozróżniać
Zanim dodasz pierwszy Cache::remember, zrozum, że „cache" w aplikacji webowej to trzy różne warstwy — każda z innym narzędziem.
| Warstwa | Co cache'uje | Narzędzie |
|---|---|---|
| Opcode cache | skompilowane pliki PHP | OPcache (konfiguracja) |
| Cache aplikacyjny | wyniki zapytań, obliczenia, fragmenty widoków | Cache::remember, @cache |
| Cache HTTP | całe odpowiedzi dla anonimowych użytkowników | response cache, CDN, Cache-Control |
Ten artykuł dotyczy dwóch ostatnich. Kluczowa zasada: cache nie tworzy danych, tylko je przechowuje — więc projektujesz go razem z pytaniem „kiedy te dane przestają być prawdziwe?".
remember, rememberForever i flexible — API, które pokrywa 90% przypadków
// Wynik na godzinę
$stats = Cache::remember('dashboard:stats', now()->addHour(), function () {
return Order::query()
->selectRaw('status, count(*) as total, sum(total) as revenue')
->groupBy('status')
->get();
});
// Dane konfiguracyjne — bez wygasania (unieważniane ręcznie)
$settings = Cache::rememberForever('app:settings', fn () => Setting::pluck('value', 'key'));
// Flexible — świeże dla użytkownika, odświeżane w tle po wygaśnięciu
$categories = Cache::flexible('categories:tree', [300, 3600], fn () =>
Category::with('children')->whereNull('parent_id')->get()
);
flexible() (Laravel 11+) to wzorzec stale-while-revalidate: pierwsza wartość czasu to „świeżość", druga — „ważność". Po upływie świeżości użytkownik dostaje starą wartość, a aplikacja odświeża cache w tle. Idealne dla danych, które mogą być o kilka minut nieaktualne (rankingi, liczniki).
Wskazówka: Klucze cache projektuj jak schemat:
{domena}:{typ}:{id}— np.products:list:page:3,user:42:permissions. Łatwiej je wtedy znajdować, unieważniać i monitorować. Zawsze wersjonuj format klucza (v2:), gdy zmieniasz strukturę cache'owanej wartości.
Cache tagów — selektywne unieważnianie (tylko Redis i Memcached)
Cache::flush() czyści wszystko — w produkcji to akt o desperacji: tracisz cały cache aplikacji, żeby usunąć jedną nieaktualną listę. Tagi pozwalają unieważniać grupy:
// Zapis z tagami
Cache::tags(['products', 'category:5'])->remember(
"products:category:5:page:1",
now()->addHour(),
fn () => Product::where('category_id', 5)->paginate(24)
);
// Unieważnienie tylko produktów kategorii 5
Cache::tags(['category:5'])->flush();
// Wszystkie produkty (zmiana cennika, import)
Cache::tags(['products'])->flush();
Uwaga: Tagi działają wyłącznie na Redisie i Memcached. Na sterownikach
fileidatabaseCache::tags()rzuci wyjątekBadMethodCallException. Jeśli architektura cache opiera się na tagach, wybór Redisa nie jest opcją — jest wymaganiem. Konfigurację Redisa opisuję w Redis w Laravel — cache, kolejki i sesje.
Wzorce unieważniania — event-driven i wersjonowanie kluczy
Dwa podejścia do inwalidacji bez tagów
1. Unieważnianie po zdarzeniu modelu — observer czyści dokładnie to, co trzeba
class ProductObserver
{
public function saved(Product $product): void
{
Cache::forget("product:{$product->id}");
Cache::tags(['products'])->flush(); // listy produktów
}
}
2. Wersjonowanie kluczy — zamiast usuwać klucze, zmieniasz „generację" w kluczu
$version = Cache::get('products:version', 1);
$key = "products:v{$version}:list:page:3";
// „Unieważnienie" = inkrementacja wersji (stare klucze wygasną same)
Cache::increment('products:version');
Wersjonowanie jest odporne na wyścigi (race conditions) między serwerami aplikacji i nie wymaga tagów — kosztem pamięci: stare wpisy żyją do wygaśnięcia TTL.
Cache widoków — @cache w Blade
Ciężkie fragmenty widoków (menu, stopka z danymi z bazy, sidebar z kategoriami) renderują się przy każdym żądaniu. Dyrektywa @cache zapamiętuje gotowy HTML:
@cache('sidebar:categories', now()->addHours(6))
<aside>
@foreach ($categories as $category)
<a href="{{ route('category', $category) }}">{{ $category->name }}</a>
@endforeach
</aside>
@endcache
- Uwaga na kontekst:
@cachezapamiętuje gotowy HTML, więc nie używaj go dla fragmentów zależnych od zalogowanego użytkownika (imię, koszyk, uprawnienia). Dla treści per-user cache jest możliwy, ale klucz musi zawierać ID użytkownika — a przy tysiącach użytkowników lepszy bywa inny wzorzec (cache danych, nie HTML).
Response cache — całe odpowiedzi HTTP
Strony publiczne o identycznej treści dla wszystkich (blog, landingi, katalog, strony usług) można cache'ować w całości — z pominięciem PHP, bazy i Blade. Pakiet spatie/laravel-responsecache zapamiętuje pełną odpowiedź i unieważnia ją po zapisie modelu:
// Middleware na wybranych routach
Route::middleware('cacheResponse:3600')->group(function () {
Route::get('/blog', [BlogController::class, 'index']);
Route::get('/blog/{slug}', [BlogController::class, 'show']);
});
To największy zysk „na żądanie": odpowiedź z cache to kilka milisekund zamiast dziesiątek lub setek. Warstwy wyżej: Varnish albo Nginx FastCGI Cache (cache przed PHP) i CDN (cache przed serwerem). Każda warstwa niżej = mniej pracy dla warstw poniżej.
Uwaga: Response cache i
@cachenie mogą obejmować treści zależnej od sesji (koszyk, „zalogowany jako", CSRF tokeny w formularzach). Cache'owanie strony z tokenem CSRF dla użytkownika A pokaże go użytkownikowi B. Filtruj routy świadomie — tylko publiczne GET-y bez stanu.
Cache warmup — żeby pierwszy użytkownik nie płacił
Po deployu (i po Cache::flush()) cache jest zimny: pierwsze żądania każdej strony płacą pełny koszt. Warmup to skrypt, który „przechodzi" najważniejsze widoki po wdrożeniu:
# Prosty warmup kluczowych URL-i po deployu
for url in / /blog /cennik /uslugi/laravel-development; do
curl -s -o /dev/null "https://example.com${url}"
done
W aplikacji możesz też precyzyjnie odgrzać konkretne klucze komendą artisan (własna php artisan cache:warmup, która wywołuje te same Cache::remember, co kod produkcyjny). Warmup wpina się na koniec pipeline'u CI/CD.
Pułapki, które kosztują najwięcej
- Cache stampede — gdy popularny klucz wygaśnie, sto równoległych żądań liczy tę samą wartość naraz. Rozwiązania:
Cache::flexible(), blokady atomowe (Cache::lock()), losowy jitter w TTL. - Cache'owanie null —
Cache::rememberznullwynikiem zapamiętanulli będzie go zwracać. Świadomie decyduj, czy „brak wyniku" też cache'ujesz. - TTL bez planu — „ustawię godzinę i zobaczymy" to nie strategia. Dla każdego klucza wiedz, jak długo dane mogą być nieaktualne — to determinuje TTL i mechanizm unieważniania.
- Cache jako źródło prawdy — jeśli po
flush()aplikacja nie działa poprawnie, cache maskuje błąd architektury. Aplikacja musi działać bez cache (wolniej), a nie tylko z nim.
Cache a cała strategia wydajności
Cache jest trzecią warstwą optymalizacji — po eliminacji N+1 i dobraniu zapytań Eloquenta. Kolejność ma znaczenie: cache'owanie N+1 to zamrażanie problemu, nie jego rozwiązanie. Pełną mapę optymalizacji — od zapytań po Octane — znajdziesz w optymalizacji wydajności aplikacji Laravel.
Najczęściej zadawane pytania
Co cache'ować w aplikacji Laravel w pierwszej kolejności?
Dane drogie do policzenia i rzadko zmieniane: agregaty (statystyki, liczniki), drzewa kategorii, konfiguracje, listy słownikowe, ciężkie fragmenty widoków. Druga fala: całe odpowiedzi publicznych stron (response cache). Nie zaczynaj od cache'owania wszystkiego — zacznij od najdroższych zapytań z profilu.
Czym różni się Cache::remember od Cache::flexible?
remember zwraca wartość i odświeża ją po wygaśnięciu TTL — pierwsze żądanie po wygaśnięciu czeka na przeliczenie. flexible (stale-while-revalidate) zwraca nieświeżą wartość natychmiast i odświeża ją w tle — użytkownik nigdy nie czeka, kosztem kilkuminutowej nieaktualności danych.
Jak unieważnić cache po zmianie danych?
- Trzy poziomy:
Cache::forget($key)dla pojedynczego klucza,Cache::tags([...])->flush()dla grup (tylko Redis/Memcached) i wersjonowanie kluczy (inkrementacja „generacji" w kluczu) dla systemów rozproszonych. NigdyCache::flush()w produkcji — to czyści cały cache aplikacji.
Czy cache tagów działa na sterowniku file?
Nie — Cache::tags() wymaga Redisa lub Memcached. Na file i database rzuci wyjątek. To jeden z powodów, dla których Redis jest standardem produkcyjnym: tagi, blokady atomowe i wspólny cache dla wielu serwerów.
Jak długi TTL ustawić?
Tak długi, jak długo dane mogą być nieaktualne bez szkody dla biznesu: konfiguracje — godziny/dni, agregaty — minuty/godziny, listy produktów — godziny. TTL to kompromis między świeżością a obciążeniem bazy — ustal go świadomie per klucz, nie globalnie.
Jak zabezpieczyć się przed cache stampede?
- Trzy techniki:
Cache::flexible()(odświeżanie w tle),Cache::lock()(tylko jeden proces liczy wartość, reszta czeka) i losowy jitter w TTL (klucze nie wygasają równocześnie). Przy popularnych kluczach i dużym ruchu stosuj co najmniej jedną.
👉 Baza danych nie wyrabia, a cache budzi wątpliwości? Zaprojektujemy strategię cache z unieważnianiem — opisz projekt przez formularz. Zobacz też utrzymanie techniczne i SLA.
📖 Zobacz również: Redis w Laravel — cache, kolejki i sesje, optymalizacja wydajności aplikacji Laravel, wydajność Eloquent oraz load testing i profilowanie.