# Cache'owanie i renderowanie w Next.js w 2026: z wersji 14 na 16

> Te same pytania co w 2025, nowe odpowiedzi: SSG, ISR, SSR, PPR i rewalidacja w Next.js 16 z Cache Components. Plus co zostało z mojej pułapki.

- URL: https://www.maciejbednarczyk.com/blog/nextjs-caching-rendering-2026
- Author: Maciej Bednarczyk
- Language: pl
- Published: 2026-11-10

## Te same pytania, nowe odpowiedzi

W maju 2025 [opisałem, jak Next.js renderuje i cache'uje strony](https://www.maciejbednarczyk.com/blog/mastering-caching-and-rendering-in-next-js): SSG, ISR, SSR, PPR, CSR, `react.cache()` i ręczną rewalidację, każde z przykładem. Ten tekst był pisany pod Next.js 14 i model, w którym cache'owaniem sterowały `export const revalidate` i `export const dynamic`.

Od Next.js 16.4 (6 października 2026) zespół Next.js rekomenduje dla każdej aplikacji Cache Components, a `create-next-app` włącza je od razu. W Next.js 17 ma to być jedyny model.

Dlatego ten post idzie dokładnie tą samą drogą co tamten. Sekcja po sekcji: najpierw jak to robiłem w Next.js 14, potem jak to robię dziś w 16 i dlaczego. Pułapka, która w 2025 kosztowała mnie najwięcej czasu, też jest tam, gdzie była, w sekcji o ISR. Tylko wygląda już inaczej.

Wszystkie komunikaty błędów i wyniki pomiarów w tym tekście pochodzą z małego demo, które zbudowałem w Next.js 16.4.6 (`next build` + `next start`).

* * *

## Co się zmieniło, w jednym akapicie?

W Next.js 14 strona była domyślnie statyczna, a ty wyłączałeś cache tam, gdzie przeszkadzał. W 16 jest odwrotnie: wszystko jest dynamiczne, a cache włączasz jawnie tam, gdzie go chcesz, dyrektywą `"use cache"`. Next.js składa z tego statyczną powłokę strony i dynamiczne fragmenty w `<Suspense>`, które dociągają się przy żądaniu.

![Next.js 14 kontra 16: jeden czas życia dla całej strony kontra statyczna powłoka, w której każdy fragment ma własny cacheLife](https://assets.cmssy.io/6ae4795c65113d877fc8d877/2cfcf179-af01-46fe-be75-d2dbdaf56448-nextjs-cache-2026-01-next14-vs-next16-pl.png)

Cały model włączają dwie flagi. Bez `partialPrefetching` Next.js 16.4 wypisuje ostrzeżenie.

```typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
};

export default nextConfig;
```

* * *

## Czym jest cache w Next.js dziś?

Idea się nie zmieniła: zapamiętujesz wynik, żeby nie liczyć go drugi raz. Zmieniło się to, kto decyduje. W 14 decydowała konfiguracja segmentu i opcje `fetch`. W 16 decydujesz ty, trzema wywołaniami w kodzie:

-   `"use cache"` oznacza, co jest cache'owane: funkcję z danymi albo cały komponent;
-   `cacheLife()` mówi, jak długo;
-   `cacheTag()` mówi, jak to unieważnić.

```tsx
import { cacheLife, cacheTag } from "next/cache";

// Poziom danych: jedna funkcja, używana na wielu stronach
export async function getPosts() {
  "use cache";
  cacheLife("days");
  cacheTag("posts");
  return db.post.findMany();
}

// Poziom UI: cache'owany jest wyrenderowany komponent
async function PostList() {
  "use cache";
  cacheLife("hours");
  const posts = await getPosts();
  return (
    <ul>
      {posts.map((p) => <li key={p.id}>{p.title}</li>)}
    </ul>
  );
}
```

`cacheLife` przyjmuje gotowy profil albo obiekt z `stale`, `revalidate` i `expire`:

<table><thead><tr><th>Profil</th><th>stale</th><th>revalidate</th><th>expire</th></tr></thead><tbody><tr><td>seconds</td><td>30 s</td><td>1 s</td><td>1 min</td></tr><tr><td>minutes</td><td>5 min</td><td>1 min</td><td>1 h</td></tr><tr><td>hours</td><td>5 min</td><td>1 h</td><td>1 dzień</td></tr><tr><td>days</td><td>5 min</td><td>1 dzień</td><td>1 tydzień</td></tr><tr><td>weeks</td><td>5 min</td><td>1 tydzień</td><td>30 dni</td></tr><tr><td>max</td><td>5 min</td><td>30 dni</td><td>1 rok</td></tr></tbody></table>

Zapamiętaj jeden próg, wraca w każdej sekcji niżej. Cache z `revalidate: 0` albo `expire` poniżej 5 minut jest krótkożyjący: nie trafia do powłoki i staje się dynamiczną dziurą. Z gotowych profili dotyczy to tylko `seconds`.

* * *

## Strona statyczna (SSG)

**W Next.js 14** strona bez dynamicznych danych była statyczna sama z siebie, a jeśli chciałeś mieć pewność, wymuszałeś to:

```tsx
export const dynamic = "force-static";

export default function About() {
  return <h1>O mnie</h1>;
}
```

**W Next.js 16** wystarczy, że strona nie czyta niczego z żądania. Trafia wtedy w całości do statycznej powłoki przy buildzie. Dane z bazy czy CMS-a dokładasz w `"use cache"`, żeby też weszły do powłoki:

```tsx
import { cacheLife } from "next/cache";

async function Bio() {
  "use cache";
  cacheLife("max");
  const bio = await cms.getBio();
  return <p>{bio.text}</p>;
}

export default function About() {
  return (
    <main>
      <h1>O mnie</h1>
      <Bio />
    </main>
  );
}
```

`force-static` nie ma prostego odpowiednika. Zamiast wymuszać statyczność, możesz jej pilnować: `ensureStatic` wywala build, jeśli na stronę trafi cokolwiek dynamicznego. Najlepiej dodać go w layoucie całej sekcji, np. bloga:

```tsx
export const ensureStatic = "navigation";
```

Sprawdziłem to w Next.js 16.4.6: dodałem do takiej strony komponent czytający `cookies()` w `<Suspense>`. Build się zatrzymał:

```
Error: Route "/static-guard": Next.js encountered uncached or runtime data on a route that must be fully static.
This route is configured to be fully static, but some data requires rendering at request time.
```

To dokładnie ta kontrola, której brakowało mi w 2025.

* * *

## Przyrostowa regeneracja (ISR)

**W Next.js 14** ISR to była statyczna strona z zegarem. `revalidate` mówił, co ile sekund ją przebudować, a `generateStaticParams` które ścieżki wygenerować przy buildzie:

```tsx
export const revalidate = 3600;

export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((p) => ({ slug: p.slug }));
}

export default async function Post({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return <Article post={post} />;
}
```

**W Next.js 16** zegar przenosi się z pliku do danych. `generateStaticParams` zostaje, a `revalidate` zastępuje `cacheLife`:

```tsx
import { cacheLife, cacheTag } from "next/cache";

export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((p) => ({ slug: p.slug }));
}

async function getPost(slug: string) {
  "use cache";
  cacheLife("hours");
  cacheTag("posts", `post-${slug}`);
  return cms.getPost(slug);
}

export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await getPost(slug);
  return <Article post={post} />;
}
```

Starego `export const revalidate` nie da się zostawić. Przy włączonych Cache Components build kończy się błędem, osobno dla `revalidate` i dla `dynamic`:

```
Error: Route segment config "revalidate" is not compatible with `nextConfig.cacheComponents`. Please remove it.
```

To dobra wiadomość przy migracji: kompilator sam pokaże wszystkie miejsca do przepisania.

### Pułapka z 2025: najkrótszy czas wygrywa. Co z niej zostało?

W 2025 ostrzegałem: Next.js bierze najkrótszy `revalidate` z całego drzewa renderowania. Jeden `fetch` z minutą w layoucie i strona, która miała żyć tydzień, przebudowuje się co minutę. Ostrzeżenia brak. Jedyny sposób to patrzeć na nagłówek `age` w zakładce Network.

W nowym modelu ta pułapka rozpadła się na dwie.

**Wersja głośna.** Krótkożyjący cache w środku zewnętrznego `"use cache"`, który nie ma własnego `cacheLife`, wywala prerender:

```tsx
async function LiveStatus() {
  "use cache";
  cacheLife("seconds");
  const status = await getStatus();
  return <p>Status: {status.state}</p>;
}

export default async function DocsPage() {
  "use cache"; // brak cacheLife
  const docs = await getDocs();
  return (
    <main>
      <Docs docs={docs} />
      <LiveStatus />
    </main>
  );
}
```

```
Error: Route "/docs": A nested `"use cache"` with a short `expire` (under 5 minutes) is inside
an outer `"use cache"` that has no `cacheLife()`. Add `cacheLife()` to the outer one to choose
whether to prerender it with a longer `expire` or keep it dynamic with a short `expire`.
```

To dobra zmiana. To, czego w 2025 trzeba było szukać w nagłówkach, teraz zatrzymuje build.

**Wersja cicha.** Kiedy zewnętrzny scope ma własny `cacheLife`, wygrywa on, nawet jeśli jest dłuższy. Pasek statusu z `minutes` w layoucie z `hours` pokazuje dane sprzed godziny:

```tsx
export default async function DocsLayout({ children }: { children: React.ReactNode }) {
  "use cache";
  cacheLife("hours"); // ten decyduje
  return (
    <section>
      <StatusBar /> {/* "use cache" + cacheLife("minutes") */}
      {children}
    </section>
  );
}
```

Brak błędu, brak ostrzeżenia, brak wpisu w dev overlay. Zasada z 2025 się odwróciła: wtedy wygrywał najkrótszy czas, teraz wygrywa zewnętrzny.

![Ten sam pasek statusu: wewnątrz layoutu z cacheLife("hours") stoi w miejscu, obok layoutu odświeża się co minutę](https://assets.cmssy.io/6ae4795c65113d877fc8d877/192cd9d7-51ca-42be-86fa-73080939c499-nextjs-cache-2026-04-frozen-status-bar-pl.gif)

Sprawdziłem to w Next.js 16.4.6 na buildzie produkcyjnym (`next start`). Przez 16 minut przeładowywałem co minutę dwie wersje tej samej strony:

<table><thead><tr><th>Przeładowanie</th><th>Pasek wewnątrz layoutu z hours</th><th>Pasek obok layoutu</th></tr></thead><tbody><tr><td>08:38</td><td>08:35:53</td><td>08:37:10</td></tr><tr><td>08:41</td><td>08:35:53</td><td>08:40:22</td></tr><tr><td>08:44</td><td>08:35:53</td><td>08:43:22</td></tr><tr><td>08:47</td><td>08:35:53</td><td>08:46:22</td></tr><tr><td>08:50</td><td>08:35:53</td><td>08:49:22</td></tr><tr><td>08:53</td><td>08:35:53</td><td>08:52:22</td></tr></tbody></table>

08:35:53 to moment builda. Pasek wewnątrz layoutu nie zmienił się ani razu. Pasek obok pokazuje czas sprzed minuty, bo tak działa stale-while-revalidate: przeładowanie dostaje poprzednią wersję i zleca policzenie nowej.

Jedno miejsce jednak tę pułapkę zdradza: tabela tras na końcu `next build`. Kolumna Revalidate pokazuje czas, który naprawdę obowiązuje dla trasy:

```
Route (app)          Revalidate  Expire
├ ○ /trap2-fixed             1m      1h
├ ○ /trap2-silent            1h      1d
```

Jeśli pasek miał się odświeżać co minutę, a trasa ma `1h`, to właśnie wpadłeś w wersję cichą.

**Rozwiązanie dla obu wersji jest to samo:** nie zagnieżdżaj krótszego cache'u w dłuższym. Postaw je obok siebie:

```tsx
export default function DocsLayout({ children }: { children: React.ReactNode }) {
  return (
    <section>
      <DocsShell /> {/* "use cache" + cacheLife("hours") */}
      <Suspense fallback={<p>Ładuję status…</p>}>
        <StatusBar /> {/* "use cache" + cacheLife("minutes") */}
      </Suspense>
      {children}
    </section>
  );
}
```

* * *

## Renderowanie na żądanie (SSR)

**W Next.js 14** całą stronę przełączałeś w tryb dynamiczny jednym eksportem:

```tsx
export const dynamic = "force-dynamic";

export default async function Dashboard() {
  const user = await getCurrentUser();
  return <h1>Cześć, {user.name}</h1>;
}
```

**W Next.js 16** dynamiczność jest domyślna, ale już nie dla całej strony, tylko dla fragmentu. Część czytająca żądanie idzie do `<Suspense>`, reszta zostaje w statycznej powłoce:

```tsx
import { Suspense } from "react";
import { cookies } from "next/headers";

async function Greeting() {
  const session = (await cookies()).get("session")?.value;
  const user = await getUserBySession(session);
  return <h1>Cześć, {user.name}</h1>;
}

export default function Dashboard() {
  return (
    <main>
      <Suspense fallback={<h1>Cześć!</h1>}>
        <Greeting />
      </Suspense>
      <DashboardNav /> {/* statyczne, w powłoce */}
    </main>
  );
}
```

Jeśli dynamiczny fragment nie jest w `<Suspense>`, Next.js zgłasza błąd zamiast po cichu renderować całą stronę na żądanie.

### Uwaga: znacznik czasu też jest danymi z żądania

`new Date()`, `Date.now()`, `Math.random()` i `crypto.randomUUID()` poza cache'em blokują prerender. Najpierw zdecyduj, o który czas ci chodzi:

```tsx
import { connection } from "next/server";

// Czas tego żądania: dynamiczna dziura
async function RequestTime() {
  await connection();
  return <p>{new Date().toISOString()}</p>;
}

// Czas zbudowania treści: cache'owany razem z nią
async function BuiltAt() {
  "use cache";
  cacheLife("hours");
  return <p>{new Date().toISOString()}</p>;
}
```

Bez `connection()` build zatrzymuje się z bardzo konkretnym komunikatem, który od razu podaje trzy wyjścia. W `next dev` ten sam błąd pojawia się przy pierwszym wejściu na stronę:

```
Error: Route "/time": Next.js encountered the unstable value `new Date()` while prerendering.
Ways to fix this:
  - [dynamic] Render at request time by adding a dynamic data access (e.g. `await connection()`)
  - [cache] Prerender and cache the value with "use cache"
  - [client] Render the value on the client with "use client"
```

* * *

## Strona mieszana (dawny PPR)

W 2025 Partial Prerendering było osobną, eksperymentalną funkcją. W Next.js 16 to po prostu sposób, w jaki działa każda strona: statyczna powłoka plus dynamiczne dziury w `<Suspense>`. Nie ma czego włączać osobno.

![Strona produktu w Next.js 16: co trafia do statycznej powłoki, a co jest dynamiczną dziurą](https://assets.cmssy.io/6ae4795c65113d877fc8d877/18e6a5ac-1fc6-45d3-b0c0-fd1f1060d1ea-nextjs-cache-2026-02-shell-and-holes-pl.png)

Dziura to komponent serwerowy, który czyta dane z żądania (`cookies()`, `headers()`, `searchParams`, `connection()`) albo używa krótkożyjącego cache'u.

```tsx
import { Suspense } from "react";
import { cookies } from "next/headers";
import { cacheLife } from "next/cache";

async function ProductInfo({ id }: { id: string }) {
  "use cache";
  cacheLife("days");
  const product = await getProduct(id);
  return <ProductDetails product={product} />; // w powłoce
}

async function Cart() {
  const cartId = (await cookies()).get("cart")?.value;
  const cart = await getCart(cartId);
  return <CartBadge count={cart.items.length} />; // dziura
}

export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  return (
    <main>
      <ProductInfo id={id} />
      <Suspense fallback={<CartBadge count={0} />}>
        <Cart />
      </Suspense>
    </main>
  );
}
```

### Poprawka do mojego przykładu z 2025

W tamtym poście jako przykład PPR pokazałem komponent kliencki, który pobiera dane w `useEffect`, owinięty w `<Suspense>`. To nie był PPR. Komponent kliencki z `useEffect` nie zawiesza renderu: trafia do powłoki i pobiera dane dopiero po hydracji w przeglądarce. To był zwykły CSR w statycznej powłoce. Dynamiczna część PPR to komponent serwerowy, jak `Cart` powyżej.

* * *

## Renderowanie po stronie klienta (CSR)

To jedyna sekcja, w której prawie nic się nie zmieniło. Komponent z `"use client"`, który pobiera dane w przeglądarce, działa w 16 tak samo jak w 14. Zmieniło się tylko otoczenie: reszta strony wokół niego jest teraz statyczną powłoką albo dynamiczną dziurą na serwerze.

Dlatego CSR ma dziś węższe zastosowanie niż w 2025. Ma sens dla danych, które zmieniają się na oczach użytkownika (wyszukiwarka z podpowiedziami, odświeżany wykres) albo które zależą od czegoś, co zna tylko przeglądarka (np. `localStorage`). Dane, które są gotowe w chwili żądania, lepiej pobrać w komponencie serwerowym w `<Suspense>`: użytkownik dostaje HTML bez czekania na JavaScript.

* * *

## react.cache() kontra "use cache"

**W Next.js 14** `react.cache()` było sposobem na deduplikację: dwa komponenty pytające o tego samego użytkownika w jednym żądaniu robiły jedno zapytanie zamiast dwóch.

**W Next.js 16** `react.cache()` robi dokładnie to samo i nadal się przydaje. Trzeba tylko pamiętać, że to są dwa różne narzędzia:

<table><thead><tr><th></th><th>react.cache()</th><th>"use cache"</th></tr></thead><tbody><tr><td>Jak długo żyje</td><td>jedno żądanie</td><td>według cacheLife, między żądaniami</td></tr><tr><td>Kto widzi wynik</td><td>tylko to żądanie</td><td>wszyscy użytkownicy</td></tr><tr><td>Może czytać cookies/sesję</td><td>tak</td><td>nie (chyba że "use cache: private")</td></tr><tr><td>Do czego</td><td>deduplikacja danych użytkownika</td><td>dane wspólne dla wszystkich</td></tr></tbody></table>

```tsx
import "server-only";
import { cache } from "react";
import { cookies } from "next/headers";

export const getCurrentUser = cache(async () => {
  const session = (await cookies()).get("session")?.value;
  return db.user.findBySession(session); // bezpośrednio do bazy
});
```

Jeszcze jedna poprawka do 2025: tamten przykład pobierał użytkownika przez `fetch` do własnego `/api/user` z komponentu serwerowego. To zbędny skok HTTP do samego siebie. Komponent serwerowy może zapytać bazę albo serwis bezpośrednio, jak wyżej.

* * *

## Ręczna rewalidacja

**W Next.js 14** tagowałeś `fetch` i unieważniałeś tag jedną funkcją, niezależnie od tego, kto ją wołał:

```tsx
const res = await fetch(url, { next: { tags: ["posts"] } });

// gdziekolwiek: Server Action albo Route Handler
revalidateTag("posts");
```

**W Next.js 16** tag dajesz w scope `"use cache"` przez `cacheTag`, a unieważnianie rozdziela się na dwa przypadki.

**Redaktor kliknął „Zapisz” w twojej aplikacji (Server Action).** Używasz `updateTag`: tag wygasa, a następny render czeka na świeże dane. Redaktor od razu widzi swoją zmianę.

```tsx
"use server";
import { updateTag } from "next/cache";

export async function savePost(formData: FormData) {
  await db.post.update(/* ... */);
  updateTag("posts");
}
```

**Headless CMS wysłał webhook (Route Handler).** `updateTag` działa tylko w Server Actions, więc tu używasz `revalidateTag` z profilem:

```typescript
import { revalidateTag } from "next/cache";

export async function POST(request: Request) {
  // najpierw zweryfikuj podpis webhooka
  revalidateTag("posts", "max");
  return Response.json({ ok: true });
}
```

Z `"max"` to stale-while-revalidate: pierwsze żądanie po webhooku dostaje jeszcze starą treść i uruchamia odświeżenie. W demo pierwsze przeładowanie po webhooku pokazało starą treść, a nowa pojawiła się dopiero przy drugim. Jeśli redaktor opublikuje w CMS-ie i od razu odświeży stronę, raz zobaczy starą wersję. Uprzedź go, zanim zgłosi błąd.

`revalidatePath` nadal działa, gdy chcesz odświeżyć konkretny adres, a nie tag.

* * *

## Co się zmienia na produkcji?

Wszystko powyżej działa na `next start` na laptopie. Produkcja dokłada dwie rzeczy, których lokalnie nie widać.

**Domyślny cache żyje w pamięci instancji.** To, co zostało wyrenderowane przy buildzie, siedzi w statycznej powłoce i jest bezpieczne. Wpis `"use cache"` utworzony w trakcie działania (dynamiczna ścieżka, rewalidacja) żyje w pamięci instancji, która go policzyła. Na serverless następne żądanie może trafić na zimną instancję i liczyć wszystko od nowa. `"use cache: remote"` przenosi wpis do współdzielonego cache'u, ale każde trafienie to wtedy żądanie sieciowe, więc opłaca się tylko przy wysokim hit rate.

**Deploy zabiera ze sobą tylko to, co powstało przy buildzie.** Strony wyrenderowane w czasie builda (np. ścieżki z `generateStaticParams`) są częścią deployu i od razu serwowane są szybko. W demo pierwsze żądanie do takiej strony trwało lokalnie 34 ms, drugie 3 ms, a w logach nie było żadnego ponownego liczenia danych. Przepadają natomiast wpisy utworzone w trakcie działania: nowe ścieżki spoza `generateStaticParams` i wszystko, co zostało przeliczone po rewalidacji. Klucze cache'u zawierają build ID, więc po deployu te wpisy liczą się od nowa, a płacą za to pierwsi użytkownicy.

* * *

## Którą strategię wybrać w 2026?

<table><thead><tr><th>Przypadek</th><th>Next.js 14</th><th>Next.js 16</th></tr></thead><tbody><tr><td>Strona „O mnie”, regulamin</td><td>force-static</td><td>nic nie czytaj z żądania; ensureStatic w layoucie</td></tr><tr><td>Blog, dokumentacja z CMS-a</td><td>ISR, revalidate</td><td>"use cache" + cacheTag + cacheLife("max") + webhook</td></tr><tr><td>Katalog produktów</td><td>ISR, revalidate: 3600</td><td>"use cache" + cacheLife("hours")</td></tr><tr><td>Strona produktu z koszykiem</td><td>PPR (eksperyment)</td><td>powłoka z produktem + koszyk w &lt;Suspense&gt;</td></tr><tr><td>Panel użytkownika</td><td>force-dynamic</td><td>dynamiczne fragmenty w &lt;Suspense&gt;, react.cache()</td></tr><tr><td>Wyszukiwarka z podpowiedziami</td><td>CSR</td><td>CSR, bez zmian</td></tr></tbody></table>

### Pełne porównanie

<table><thead><tr><th>Strategia</th><th>Świeżość danych</th><th>Wydajność</th><th>SEO</th><th>Kontrola cache'u w 16</th></tr></thead><tbody><tr><td>Statyczna powłoka</td><td>z buildu</td><td>najwyższa</td><td>tak</td><td>ensureStatic</td></tr><tr><td>"use cache" (dawne ISR)</td><td>według cacheLife</td><td>wysoka</td><td>tak</td><td>cacheLife, cacheTag</td></tr><tr><td>Dynamiczna dziura (dawne SSR)</td><td>przy każdym żądaniu</td><td>zależy od źródła</td><td>tak</td><td>&lt;Suspense&gt;, connection()</td></tr><tr><td>CSR</td><td>w przeglądarce</td><td>zależy od klienta</td><td>słabe</td><td>poza Next.js</td></tr><tr><td>react.cache()</td><td>jedno żądanie</td><td>deduplikacja</td><td>nie dotyczy</td><td>nie dotyczy</td></tr></tbody></table>

![Schemat decyzyjny: którą strategię cache'owania wybrać w Next.js 16](https://assets.cmssy.io/6ae4795c65113d877fc8d877/c65194f9-fe7f-4d0f-86a1-41b850903620-nextjs-cache-2026-03-which-strategy-pl.png)

### Szybkie zasady na 2026

1.  Każdy `"use cache"` dostaje jawny `cacheLife`.
2.  Nie zagnieżdżaj krótszego cache'u w dłuższym. Stawiaj je obok siebie.
3.  Treść z CMS-a: `cacheTag` + `max` + webhook z `revalidateTag(tag, "max")`. `updateTag` zostaw dla Server Actions.
4.  Sekcja, która musi być statyczna, dostaje `ensureStatic = "navigation"` w layoucie.
5.  Cache'owanie sprawdzaj na buildzie produkcyjnym i na platformie, na której wdrażasz, a nie w `next dev`. Zerknij też na kolumnę Revalidate w wyniku `next build`.

Next.js 17 ma zrobić z Cache Components jedyny model. Jeśli migrujesz teraz, robisz to raz.