W poprzednim module poznaliśmy podstawy rewalidacji danych. Teraz czas zagłębić się w zaawansowane mechanizmy cachowania, które Next.js App Router oferuje pod maską. Zrozumienie tych warstw to klucz do tworzenia aplikacji, które działają błyskawicznie — niczym systemy w cyberpunkowym mieście Quantum, gdzie każda milisekunda opóźnienia może kosztować życie.
Next.js App Router implementuje wielowarstwowy system cachowania. Każda warstwa ma inne zadanie i inny czas życia:
fetch zostanie wywołany wielokrotnie w ramach jednego renderowania (np. w różnych komponentach), Next.js wykona go tylko raz.fetch. Przetrwa między kolejnymi żądaniami użytkowników, a nawet między deploymentami. To właśnie ten cache kontrolujesz przez revalidate i cache: 'no-store'.Gdy użytkownik odwiedza stronę, Next.js sprawdza cache w następującej kolejności:
1Router Cache (przeglądarka)
2 ↓ miss
3Full Route Cache (serwer)
4 ↓ miss
5Renderowanie strony → Data Cache → Request Memoization → fetch do APIZrozumienie tego przepływu pozwala precyzyjnie kontrolować, które dane są świeże, a które mogą być serwowane z cache.
Nie wszystkie dane pochodzą z
fetch. Gdy pobierasz dane z bazy danych (np. przez Prisma, Drizzle czy bezpośrednie zapytania), fetch nie jest zaangażowany i standardowy Data Cache nie działa. Do tego służy unstable_cache:1import { unstable_cache } from 'next/cache';
2
3// Cachowanie zapytania do bazy danych
4const getCachedUser = unstable_cache(
5 async (userId: string) => {
6 // To zapytanie zostanie zcachowane
7 const user = await db.user.findUnique({
8 where: { id: userId },
9 include: { posts: true, profile: true }
10 });
11 return user;
12 },
13 // Klucz cache — tablica stringów identyfikujących ten cache
14 ['user-data'],
15 {
16 // Tagi do rewalidacji na żądanie
17 tags: ['users'],
18 // Czas rewalidacji w sekundach
19 revalidate: 3600
20 }
21);
22
23// Użycie w Server Component
24export default async function UserProfile({ params }: { params: Promise<{ id: string }> }) {
25 const user = await getCachedUser((await params).id);
26
27 return (
28 <div>
29 <h1>{user.name}</h1>
30 <p>{user.profile?.bio}</p>
31 <h2>Posty ({user.posts.length})</h2>
32 </div>
33 );
34}unstable_cache przyjmuje trzy argumenty:tags (do rewalidacji na żądanie) i revalidate (czas w sekundach)Tagi cache pozwalają grupować powiązane dane i rewalidować je razem. Jest to potężniejsze niż
revalidatePath, bo działa niezależnie od ścieżek URL.1// Dane produktu — oznaczone tagami ogólnym i specyficznym
2async function getProduct(id: string) {
3 const res = await fetch(`https://api.example.com/products/${id}`, {
4 next: {
5 tags: [
6 'products', // tag ogólny — rewaliduje wszystkie produkty
7 `product-${id}` // tag specyficzny — rewaliduje tylko ten produkt
8 ]
9 }
10 });
11 return res.json();
12}
13
14// Dane kategorii — powiązane z produktami
15async function getCategory(slug: string) {
16 const res = await fetch(`https://api.example.com/categories/${slug}`, {
17 next: {
18 tags: [
19 'categories',
20 `category-${slug}`
21 ]
22 }
23 });
24 return res.json();
25}1// app/actions.ts
2'use server';
3
4import { revalidateTag, revalidatePath } from 'next/cache';
5
6// Po aktualizacji jednego produktu
7export async function updateProduct(id: string, data: ProductData) {
8 await db.product.update({ where: { id }, data });
9
10 // Rewaliduj tylko ten konkretny produkt
11 revalidateTag(`product-${id}`);
12
13 // Rewaliduj też listę produktów (np. stronę sklepu)
14 revalidateTag('products');
15}
16
17// Po masowej aktualizacji cen
18export async function bulkUpdatePrices() {
19 await db.product.updateMany({ /* ... */ });
20
21 // Jedna komenda rewaliduje WSZYSTKIE produkty we wszystkich stronach
22 revalidateTag('products');
23}| Metoda | Zasięg | Kiedy używać | |--------|--------|-------------| |
revalidateTag('products') | Wszystkie dane z tagiem 'products', niezależnie od strony | Gdy dane pojawiają się na wielu stronach |
| revalidatePath('/products') | Cała strona /products — wszystkie dane na niej | Gdy chcesz odświeżyć konkretną stronę |
| revalidatePath('/products', 'layout') | Strona + wszystkie podstrony z tym layoutem | Gdy zmienił się layout lub dane współdzielone |Te trzy warstwy cache często są mylone. Oto kluczowe różnice:
Przechowuje wyniki
fetch na serwerze. Kontrolujesz go przez:1// Dane cachowane na stałe (domyślnie)
2fetch(url);
3
4// Dane cachowane na 60 sekund
5fetch(url, { next: { revalidate: 60 } });
6
7// Dane nigdy nie cachowane
8fetch(url, { cache: 'no-store' });Czas życia: Przetrwa między żądaniami i deploymentami (chyba że rewalidowany).
Przechowuje wygenerowany HTML + RSC Payload statycznych stron. Tworzony w czasie
next build.1// Ta strona będzie w Full Route Cache (statyczna)
2export default async function AboutPage() {
3 return <h1>O nas</h1>;
4}
5
6// Ta strona NIE będzie w Full Route Cache (dynamiczna)
7export default async function DashboardPage() {
8 const session = await getSession(); // dynamic function
9 return <h1>Witaj, {session.user.name}</h1>;
10}
11
12// Wymuszenie rewalidacji Full Route Cache
13export const revalidate = 3600; // strona rewalidowana co godzinęCzas życia: Do następnego
next build lub rewalidacji.Przechowuje odwiedzone strony w przeglądarce użytkownika. Dzięki niemu nawigacja wstecz jest natychmiastowa.
1'use client';
2
3import { useRouter } from 'next/navigation';
4
5export function RefreshButton() {
6 const router = useRouter();
7
8 return (
9 <button onClick={() => {
10 // Wyczyść Router Cache i pobierz świeże dane z serwera
11 router.refresh();
12 }}>
13 Odśwież dane
14 </button>
15 );
16}Czas życia:
Ważne rozróżnienie, które wielu programistów pomija:
Działa tylko w ramach jednego żądania serwerowego. Jeśli komponent A i komponent B wywołują ten sam
fetch, zostanie on wykonany tylko raz:1// Oba komponenty współdzielą ten sam fetch w ramach jednego renderowania
2async function Header() {
3 const user = await getUser(); // fetch #1
4 return <nav>{user.name}</nav>;
5}
6
7async function Sidebar() {
8 const user = await getUser(); // ten sam fetch — z memoizacji, nie z sieci!
9 return <aside>{user.avatar}</aside>;
10}Przetrwa między żądaniami różnych użytkowników:
1async function getPopularPosts() {
2 // Te dane są współdzielone między WSZYSTKIMI użytkownikami
3 const res = await fetch('https://api.example.com/popular', {
4 next: { revalidate: 300 } // cache na 5 minut
5 });
6 return res.json();
7}Problemy z cache są jednymi z najtrudniejszych do zdiagnozowania. Oto sprawdzone techniki:
1export default async function Page() {
2 console.log('[Page] Rendering at:', new Date().toISOString());
3
4 const data = await getData();
5 console.log('[Page] Data fetched, first item:', data[0]?.id);
6
7 return <div>{/* ... */}</div>;
8}Jeśli widzisz ten sam timestamp przy odświeżaniu — strona jest serwowana z Full Route Cache.
W trybie produkcyjnym Next.js dodaje nagłówek
x-nextjs-cache do odpowiedzi:HIT — strona z Full Route CacheMISS — strona renderowana na żywoSTALE — strona z cache, ale rewalidacja została wyzwolona w tle1import { headers } from 'next/headers';
2
3export default async function DebugPage() {
4 // Użycie await headers() wymusza dynamiczne renderowanie
5 const headersList = await headers();
6
7 return (
8 <div>
9 <p>Rendered: {new Date().toISOString()}</p>
10 <p>Ten timestamp powinien się zmieniać przy każdym odświeżeniu</p>
11 </div>
12 );
13}Zaawansowane strategie cachowania w Next.js tworzą wielowarstwowy system, który — dobrze skonfigurowany — potrafi radykalnie przyspieszyć aplikację. Kluczowe wnioski:
unstable_cache — pozwala cachować dane z baz danych i innych źródeł nieopartych na fetchrevalidateTag dla danych, revalidatePath dla stronx-nextjs-cache, wymuszanie dynamicznego renderowania