Jak działa cache w Next.js v13+ z App Router

Cache w Next.js 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

Przykład użycia Next.js

Źródło: Next.js Documentation

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 lub revalidateTag().

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 } lub cache: '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' czy no-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. w layout.tsx i page.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 cacheGdzie działaZarządzaniePrzykład
Full Route CacheSerwer (cała trasa)revalidate, tagexport const revalidate = 60
Data CacheSerwer (fetch)cache, revalidatefetch(..., { next: ... })
Static Gen CacheSerwer (build time)BrakJSX bez danych
Client-Side CachePrzeglądarkaSamodzielnieSWR / React Query

Przykład z życia

Załóżmy, że tworzysz sklep:

StronaCo cache’owaćJaki typ cache wybrać
/productsLista produktów, aktualizowana co 5 minFull Route Cache (ISR)
/api/productsDane API produktówData Cache (przez revalidateTag)
Nawigacja SPASzybka nawigacjaRouter 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()