Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Zaawansowane strategie cachowania w Next.js

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:

  1. Request Memoization — cache w pamięci na czas trwania jednego żądania serwerowego. Jeśli ten sam
    fetch
    zostanie wywołany wielokrotnie w ramach jednego renderowania (np. w różnych komponentach), Next.js wykona go tylko raz.
  2. 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 przez
    revalidate
    i
    cache: 'no-store'
    .
  3. Full Route Cache — cache całych stron wygenerowanych statycznie (SSG/ISR). Przechowuje gotowy HTML i React Server Component Payload na serwerze.
  4. 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 API

Zrozumienie 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) i
    revalidate
    (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 Cache
  • MISS
    — strona renderowana na żywo
  • STALE
    — 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:

  1. 4 warstwy cache — Request Memoization, Data Cache, Full Route Cache, Router Cache — każda z innym zasięgiem i czasem życia
  2. unstable_cache
    — pozwala cachować dane z baz danych i innych źródeł nieopartych na
    fetch
  3. Cache tags — precyzyjna kontrola rewalidacji:
    revalidateTag
    dla danych,
    revalidatePath
    dla stron
  4. Per-request vs cross-request — Request Memoization działa w ramach jednego renderowania, Data Cache między żądaniami
  5. Debugowanie — logowanie timestampów, nagłówki
    x-nextjs-cache
    , wymuszanie dynamicznego renderowania
Vai a CodeWorlds