Jak działa cache w Next.js v13+ z App Router
Szczegółowo omawiamy, jak działa cache w Next.js 13+, jakie są typy cache i jak wpływają na wydajność oraz SEO.
Czym jest cache i jak działa w Next.js
Cache to sposób na zapisanie wyniku obliczeń lub zapytania, aby nie wykonywać ich ponownie i przyspieszyć działanie aplikacji. W Next.js cache’owane są strony HTML, dane, zapytania, renderowanie komponentów, a nawet całe trasy.
Wraz z przejściem na App Router (od Next.js 13) filozofia się zmieniła: cache jest domyślnie włączony, a zadaniem dewelopera jest wyłączenie go tam, gdzie potrzebna jest dynamika.
Zasada Next.js jest następująca:
"Wszystko cache’ujemy, jeśli nie powiedziano inaczej"
Jeśli nie określisz wyraźnie cache: 'no-store'
, next: { revalidate }
lub dynamic = 'force-dynamic'
, to:
fetch()
będzie cache’owany- layout i page — prawdopodobnie też
- trasy — trafią do cache tras
- render strony — do Full Route Cache
To daje wydajność, ale może prowadzić do przestarzałych danych i trudności w debugowaniu.
Typy cache w Next.js
Full Route Cache (pełne cache trasy, serwer)
Stosowane przy ISR (Incremental Static Regeneration).
- Cache’owana jest cała strona (HTML, dane z
layout.tsx
,page.tsx
,fetch()
,headers()
,cookies()
). - Cache znajduje się na serwerze.
- Działa domyślnie, jeśli nie ma dynamiki.
- Inwalidacja przez
revalidate
lubrevalidateTag()
.
Dobre dla blogów, stron marketingowych, gdzie dane rzadko się zmieniają.
Przykład:
export const revalidate = 60; // odśwież stronę co 60 sekund
Data Cache (cache zapytań fetch, serwer)
- Cache’owane są odpowiedzi
fetch()
w Server Components. - Zarządzanie przez
next: { revalidate }
lubcache: 'no-store'
.
Przykład:
await fetch('https://api.com/posts', {
next: { revalidate: 300 },
});
Request Memoization (pamięć w ramach jednego zapytania, serwer)
To wewnętrzna optymalizacja Next.js: to samo fetch()
lub funkcja z tymi samymi argumentami wywoływana jest tylko raz podczas jednego renderu.
- W ramach jednego renderu na serwerze Next.js śledzi identyczne zapytania.
- Jeśli już były wywołane — wynik pobierany jest z pamięci (in-memory).
- Niezależne od
cache: 'force-cache'
czyno-store
— działa zawsze.
Dobre dla:
- DRY (don’t repeat yourself) przy pobieraniu tych samych danych w różnych komponentach.
- Zwiększenia wydajności bez ręcznej optymalizacji.
- Gdy to samo
fetch
jest używane np. wlayout.tsx
ipage.tsx
.
Nie nadaje się do:
- Wielokrotnego użycia między różnymi zapytaniami lub użytkownikami (to nie jest globalny cache).
- Omijania cache
fetch()
— działa ponad Request Memoization.
// lib/getUser.ts
export async function getUser() {
return fetch('https://api.com/user').then(res => res.json());
}
// app/layout.tsx
export default async function Layout({ children }) {
const user = await getUser(); // Wywołanie 1
return (
<div>
<Sidebar user={user} />
{children}
</div>
);
}
// app/page.tsx
import { getUser } from '../lib/getUser';
export default async function Page() {
const user = await getUser(); // Wywołanie 2, ale faktycznie nie powtarza się
return <p>Hello, {user.name}</p>;
}
Fizycznie fetch zostanie wykonany tylko raz, a wynik zostanie użyty w obu miejscach.
Router Cache (cache po stronie klienta)
To cache, którym zarządzasz samodzielnie przez biblioteki, np. SWR, React Query itp.
- Działa tylko przy nawigacji po stronie klienta (
<Link>
,router.push()
). - Next.js nie zarządza tym cache — decydujesz sam.
- Bardzo szybki, bo nic nie jest renderowane ponownie — React po prostu montuje z pamięci.
- Ale działa tylko do czasu odświeżenia strony (F5).
Dobre dla komponentów interaktywnych, formularzy, dashboardów.
Przykład:
'use client';
import useSWR from 'swr';
const { data } = useSWR('/api/data', fetcher);
Porównanie typów cache
Typ cache | Gdzie działa | Zarządzanie | Przykład |
---|---|---|---|
Full Route Cache | Serwer (cała trasa) | revalidate , tag | export const revalidate = 60 |
Data Cache | Serwer (fetch) | cache , revalidate | fetch(..., { next: ... }) |
Static Gen Cache | Serwer (build time) | Brak | JSX bez danych |
Client-Side Cache | Przeglądarka | Samodzielnie | SWR / React Query |
Przykład z życia
Załóżmy, że tworzysz sklep:
Strona | Co cache’ować | Jaki typ cache wybrać |
---|---|---|
/products | Lista produktów, aktualizowana co 5 min | Full Route Cache (ISR) |
/api/products | Dane API produktów | Data Cache (przez revalidateTag ) |
Nawigacja SPA | Szybka nawigacja | Router Cache (cache po stronie klienta) |
Typowe problemy z cache w App Router
Zaktualizowałeś dane — a na stronie stare
fetch()
domyślnie używa force-cache
, jeśli nie podasz cache: 'no-store'
lub next: { revalidate }
. W efekcie Next.js cache’uje dane i nigdy ich nie odświeża.
Jak rozwiązać:
- Dodaj
cache: 'no-store'
, by wyłączyć cache - Lub
next: { revalidate: 60 }
dla ISR
fetch('https://api.com/data', { next: { revalidate: 60 } });
Zmienił się URL (searchParams), a dane te same
Next.js nie renderuje ponownie fetch, jeśli URL jest taki sam. Jeśli używasz parametrów, dodaj je do samego URL fetch:
fetch(`https://api.com/posts?category=${searchParams.category}`, {
next: { revalidate: 60 }
});
Przeszedłeś na stronę przez <Link>
, a dane się nie odświeżyły
Zadziałał Router Cache (cache po stronie klienta). Next.js nie wykonuje nowego zapytania przy powrocie na stronę.
Jak rozwiązać:
- Wymuś odświeżenie danych przez
router.refresh()
- Użyj
cache: 'no-store'
, jeśli dane muszą być zawsze świeże
revalidatePath()
lub revalidateTag()
nie działa
- Cache nie został utworzony (np. przez
no-store
) - Nie używasz
tags
w fetch - Wywołujesz
revalidatePath()
poza server action
Jak rozwiązać:
- Upewnij się, że fetch używa
next: { tags: [...] }
revalidateTag('tag')
wywołuj w funkcji serwerowej
Layout został zcache’owany i się nie odświeża
layout.tsx
też jest cache’owany jako RSC z revalidate
, i jeśli dane w środku są cache’owane — nie odświeżą się do kolejnego ISR.
Jak rozwiązać:
- Użyj
cache: 'no-store'
dla fetch w layout.tsx, jeśli np. jest tam profil użytkownika - Lub przenieś fetch z layout do page (lub client component z SWR)
Dziwne zachowanie w trybie deweloperskim (dev)
W next dev
wiele cache jest wyłączonych lub działa inaczej, revalidate
nie działa stabilnie w trybie dev.
Wskazówka:
Testuj cache przez next build && next start
lub Vercel preview.
Kombinacja cookies()
, headers()
, params
i fetch()
psuje cache
Gdy używasz dynamicznych rzeczy, takich jak:
cookies()
headers()
useSearchParams()
generateMetadata() z dynamic
Next automatycznie wyłącza cache strony i przechodzi w dynamic = 'force-dynamic'
.
Checklist: jak unikać problemów z cache
- Zawsze jawnie określaj zachowanie fetch:
// brak cache fetch(..., { cache: 'no-store' }) // ISR (odśwież co 60 sek) fetch(..., { next: { revalidate: 60 } }) // ręczne odświeżanie przez tag fetch(..., { next: { tags: ['products'] } })
- Nie zapominaj o parametrach query w samym URL fetch
- Sprawdzaj przez
next build && next start
— unikniesz fałszywego poczucia pewności - Jeśli musisz ręcznie odświeżać dane — używaj
revalidatePath()