Te same pytania, nowe odpowiedzi
W maju 2025 opisałem, jak Next.js renderuje i cache'uje strony: 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.

Cały model włączają dwie flagi. Bez partialPrefetching Next.js 16.4 wypisuje ostrzeżenie.
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ć.
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:
| Profil | stale | revalidate | expire |
|---|---|---|---|
| seconds | 30 s | 1 s | 1 min |
| minutes | 5 min | 1 min | 1 h |
| hours | 5 min | 1 h | 1 dzień |
| days | 5 min | 1 dzień | 1 tydzień |
| weeks | 5 min | 1 tydzień | 30 dni |
| max | 5 min | 30 dni | 1 rok |
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:
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:
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:
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:
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:
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:
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:
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.

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:
| Przeładowanie | Pasek wewnątrz layoutu z hours | Pasek obok layoutu |
|---|---|---|
| 08:38 | 08:35:53 | 08:37:10 |
| 08:41 | 08:35:53 | 08:40:22 |
| 08:44 | 08:35:53 | 08:43:22 |
| 08:47 | 08:35:53 | 08:46:22 |
| 08:50 | 08:35:53 | 08:49:22 |
| 08:53 | 08:35:53 | 08:52:22 |
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 1dJeś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:
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:
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:
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:
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.

Dziura to komponent serwerowy, który czyta dane z żądania (cookies(), headers(), searchParams, connection()) albo używa krótkożyjącego cache'u.
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:
| react.cache() | "use cache" | |
|---|---|---|
| Jak długo żyje | jedno żądanie | według cacheLife, między żądaniami |
| Kto widzi wynik | tylko to żądanie | wszyscy użytkownicy |
| Może czytać cookies/sesję | tak | nie (chyba że "use cache: private") |
| Do czego | deduplikacja danych użytkownika | dane wspólne dla wszystkich |
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ł:
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ę.
"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:
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?
| Przypadek | Next.js 14 | Next.js 16 |
|---|---|---|
| Strona „O mnie”, regulamin | force-static | nic nie czytaj z żądania; ensureStatic w layoucie |
| Blog, dokumentacja z CMS-a | ISR, revalidate | "use cache" + cacheTag + cacheLife("max") + webhook |
| Katalog produktów | ISR, revalidate: 3600 | "use cache" + cacheLife("hours") |
| Strona produktu z koszykiem | PPR (eksperyment) | powłoka z produktem + koszyk w <Suspense> |
| Panel użytkownika | force-dynamic | dynamiczne fragmenty w <Suspense>, react.cache() |
| Wyszukiwarka z podpowiedziami | CSR | CSR, bez zmian |
Pełne porównanie
| Strategia | Świeżość danych | Wydajność | SEO | Kontrola cache'u w 16 |
|---|---|---|---|---|
| Statyczna powłoka | z buildu | najwyższa | tak | ensureStatic |
| "use cache" (dawne ISR) | według cacheLife | wysoka | tak | cacheLife, cacheTag |
| Dynamiczna dziura (dawne SSR) | przy każdym żądaniu | zależy od źródła | tak | <Suspense>, connection() |
| CSR | w przeglądarce | zależy od klienta | słabe | poza Next.js |
| react.cache() | jedno żądanie | deduplikacja | nie dotyczy | nie dotyczy |

Szybkie zasady na 2026
- Każdy
"use cache"dostaje jawnycacheLife. - Nie zagnieżdżaj krótszego cache'u w dłuższym. Stawiaj je obok siebie.
- Treść z CMS-a:
cacheTag+max+ webhook zrevalidateTag(tag, "max").updateTagzostaw dla Server Actions. - Sekcja, która musi być statyczna, dostaje
ensureStatic = "navigation"w layoucie. - 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 wynikunext build.
Next.js 17 ma zrobić z Cache Components jedyny model. Jeśli migrujesz teraz, robisz to raz.