Kurs Next.js · Moduł 5: Strategie renderowania
Zaawansowane strategie cachowania w Next.js
W tej lekcji7
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.
Architektura warstw cache w Next.js
Next.js App Router implementuje wielowarstwowy system cachowania. Każda warstwa ma inne zadanie i inny czas życia:
- Request Memoization - cache w pamięci na czas trwania jednego żądania serwerowego. Jeśli ten sam
fetchzostanie wywołany wielokrotnie w ramach jednego renderowania (np. w różnych komponentach), Next.js wykona go tylko raz. - Data Cache - trwały cache danych pobranych przez
fetch. Przetrwa między kolejnymi żądaniami użytkowników, a nawet między deploymentami. To właśnie ten cache kontrolujesz przezrevalidateicache: 'no-store'. - Full Route Cache - cache całych stron wygenerowanych statycznie (SSG/ISR). Przechowuje gotowy HTML i React Server Component Payload na serwerze.
- Router Cache - cache po stronie przeglądarki. Przechowuje odwiedzone strony i prefetchowane segmenty, żeby nawigacja kliencka była natychmiastowa.
Jak warstwy współpracują
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.
unstable_cache - cachowanie bez fetch
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:
- Funkcję asynchroniczną - operacja do zcachowania
- Klucz cache - tablica stringów tworzących unikalny identyfikator
- Opcje -
tags(do rewalidacji na żądanie) irevalidate(czas w sekundach)
Cache tags - precyzyjna kontrola rewalidacji
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.
Strategia tagowania danych
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}Rewalidacja na żądanie z tagami
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}revalidateTag vs revalidatePath - kiedy co użyć
| 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 |
Data Cache vs Full Route Cache vs Router Cache
Te trzy warstwy cache często są mylone. Oto kluczowe różnice:
Data Cache
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).
Full Route Cache
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.
Router Cache
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:
- Strony statyczne: 5 minut
- Strony dynamiczne: 30 sekund
- Prefetched: do nawigacji
Cache scope - per-request vs cross-request
Ważne rozróżnienie, które wielu programistów pomija:
Per-request (Request Memoization)
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}Cross-request (Data Cache)
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}Debugowanie cache
Problemy z cache są jednymi z najtrudniejszych do zdiagnozowania. Oto sprawdzone techniki:
1. Logowanie w Server Components
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.
2. Nagłówek x-nextjs-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 tle
3. Wymuszanie dynamicznego renderowania do testów
1import { 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}Podsumowanie
Zaawansowane strategie cachowania w Next.js tworzą wielowarstwowy system, który - dobrze skonfigurowany - potrafi radykalnie przyspieszyć aplikację. Kluczowe wnioski:
- 4 warstwy cache - Request Memoization, Data Cache, Full Route Cache, Router Cache - każda z innym zasięgiem i czasem życia
unstable_cache- pozwala cachować dane z baz danych i innych źródeł nieopartych nafetch- Cache tags - precyzyjna kontrola rewalidacji:
revalidateTagdla danych,revalidatePathdla stron - Per-request vs cross-request - Request Memoization działa w ramach jednego renderowania, Data Cache między żądaniami
- Debugowanie - logowanie timestampów, nagłówki
x-nextjs-cache, wymuszanie dynamicznego renderowania
Kod do tej lekcji: App.tsx
1import React, { useState } from 'react';
2
3// Warstwy cache w Next.js App Router
4// Request Memoization -> Data Cache -> Full Route Cache -> Router Cache
5
6type CacheLayer = 'request-memo' | 'data-cache' | 'full-route' | 'router-cache';
7
8interface LayerInfo {
9 id: CacheLayer;
10 name: string;
11 scope: string;
12 lifetime: string;
13 description: string;
14 invalidation: string;
15 example: string;
16}
17
18const layers: LayerInfo[] = [
19 {
20 id: 'request-memo',
21 name: 'Request Memoization',
22 scope: 'Per-request (serwer)',
23 lifetime: 'Czas trwania jednego renderowania',
24 description: 'Deduplikacja identycznych fetch w ramach jednego zadania. Jesli Header i Sidebar wolaja getUser(), fetch wykona sie raz.',
25 invalidation: 'Automatycznie po zakonczeniu renderowania',
26 example: 'const user = await getUser(); // wolany 3x, wykonany 1x',
27 },
28 {
29 id: 'data-cache',
30 name: 'Data Cache',
31 scope: 'Cross-request (serwer)',
32 lifetime: 'Miedzy zadaniami i deploymentami',
33 description: 'Trwaly cache wynikow fetch. Kontrolowany przez revalidate i cache options. Wspoldzielony miedzy uzytkownikami.',
34 invalidation: 'revalidateTag() / revalidatePath() / revalidate: N',
35 example: "fetch(url, { next: { revalidate: 60, tags: ['products'] } })",
36 },
37 {
38 id: 'full-route',
39 name: 'Full Route Cache',
40 scope: 'Statyczne strony (serwer)',
41 lifetime: 'Do nastepnego buildu lub rewalidacji',
42 description: 'Cache gotowego HTML + RSC Payload dla stron statycznych. Tworzony podczas next build.',
43 invalidation: 'revalidatePath() / next build / revalidate na stronie',
44 example: "export const revalidate = 3600; // cala strona co godzine",
45 },
46 {
47 id: 'router-cache',
48 name: 'Router Cache',
49 scope: 'Przegladarka uzytkownika',
50 lifetime: 'Statyczne: 5min, Dynamiczne: 30s',
51 description: 'Cache w przegladarce dla nawigacji klienckiej. Prefetchowane strony laduja natychmiast.',
52 invalidation: 'router.refresh() / automatycznie po czasie',
53 example: "router.refresh(); // wyczysc Router Cache",
54 },
55];
56
57export default function CacheLayersDemo() {
58 const [selected, setSelected] = useState<LayerInfo>(layers[0]);
59 const [requestFlow, setRequestFlow] = useState<{ layer: string; result: 'HIT' | 'MISS' }[]>([]);
60
61 const simulateRequest = () => {
62 const flow: { layer: string; result: 'HIT' | 'MISS' }[] = [];
63 let resolved = false;
64
65 for (const layer of layers) {
66 if (resolved) break;
67 const hit = Math.random() > 0.4;
68 flow.push({ layer: layer.name, result: hit ? 'HIT' : 'MISS' });
69 if (hit) resolved = true;
70 }
71
72 if (!resolved) {
73 flow.push({ layer: 'Origin API', result: 'MISS' });
74 }
75
76 setRequestFlow(flow);
77 };
78
79 return (
80 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: 20, color: '#e0e0e0', fontFamily: 'monospace' }}>
81 <h1 style={{ color: '#64ffda', fontSize: 18 }}>Warstwy cache w Next.js</h1>
82
83 <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', margin: '16px 0' }}>
84 {layers.map((l, i) => (
85 <button key={l.id} onClick={() => setSelected(l)} style={{
86 background: selected.id === l.id ? '#1565c0' : '#1a2744',
87 color: '#fff', border: `1px solid ${selected.id === l.id ? '#64ffda' : '#333'}`,
88 borderRadius: 6, padding: '8px 12px', cursor: 'pointer', fontSize: 11, fontFamily: 'monospace'
89 }}>
90 {i + 1}. {l.name}
91 </button>
92 ))}
93 </div>
94
95 <div style={{ background: '#0d1117', borderRadius: 8, padding: 16, marginBottom: 16 }}>
96 <div style={{ color: '#64ffda', fontSize: 15, marginBottom: 8 }}>{selected.name}</div>
97 <div style={{ display: 'grid', gap: 6, fontSize: 12 }}>
98 <div><span style={{ color: '#888' }}>Scope:</span> <span style={{ color: '#ce93d8' }}>{selected.scope}</span></div>
99 <div><span style={{ color: '#888' }}>Lifetime:</span> <span style={{ color: '#ffab40' }}>{selected.lifetime}</span></div>
100 <div><span style={{ color: '#888' }}>Opis:</span> {selected.description}</div>
101 <div><span style={{ color: '#888' }}>Inwalidacja:</span> <span style={{ color: '#ef5350' }}>{selected.invalidation}</span></div>
102 <code style={{ color: '#4caf50', background: '#1a2744', padding: '6px 10px', borderRadius: 4, fontSize: 11 }}>
103 {selected.example}
104 </code>
105 </div>
106 </div>
107
108 <button onClick={simulateRequest} style={{
109 background: '#4caf50', color: '#fff', border: 'none', borderRadius: 6,
110 padding: '8px 16px', cursor: 'pointer', fontFamily: 'monospace'
111 }}>
112 Symuluj request
113 </button>
114
115 {requestFlow.length > 0 && (
116 <div style={{ marginTop: 16 }}>
117 <div style={{ color: '#888', fontSize: 12, marginBottom: 8 }}>Przeplyw zadania przez warstwy cache:</div>
118 <div style={{ display: 'flex', gap: 4, alignItems: 'center', flexWrap: 'wrap' }}>
119 {requestFlow.map((step, i) => (
120 <React.Fragment key={i}>
121 {i > 0 && <span style={{ color: '#555' }}>-></span>}
122 <div style={{
123 background: step.result === 'HIT' ? '#1b5e20' : '#b71c1c',
124 borderRadius: 4, padding: '4px 8px', fontSize: 11
125 }}>
126 {step.layer}: {step.result}
127 </div>
128 </React.Fragment>
129 ))}
130 </div>
131 </div>
132 )}
133 </div>
134 );
135}Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Do czego służy unstable_cache w Next.js App Router?
2. generateStaticParams służy do:
Zadania praktyczne w grze
- Edytor kodu
Utwórz stronę z optymalizacją: lazy loading, Suspense, proper caching, Image optimization
- Układanie w poziomie
Ułóż składnię eksportu dynamic w Next.js
- Klikanie w kolejności
Ułóż składnię komponentu Suspense z fallback w Next.js
- Edytor kodu
Utwórz Server Action do obsługi form submission z walidacją i database update