11 lipca 2026 · Piotr Józef Schumann
Cache Components i PPR w Next.js 16: co się zmieniło i jak tego używam
Przez kilka lat historia cache'owania w Next.js była stertą nakładających się na siebie eksperymentalnych flag — experimental.ppr, experimental.useCache, experimental.dynamicIO — plus konfiguracje segmentów route'a, jak export const revalidate i funkcja unstable_cache. Dało się to jakoś ogarnąć, ale wytłumaczenie juniorowi u klienta, dlaczego to działa, było drogą przez mękę.
Next.js 16 zwija to wszystko w jedną flagę: cacheComponents. To model mentalny, który chciałbym mieć wcześniej — i tak faktycznie z niego korzystam na prawdziwych stronach.
Jedna idea: dynamicznie domyślnie, cache świadomie
Z włączonym Cache Components pobieranie danych jest domyślnie dynamiczne. Nic nie jest cache'owane, dopóki tego nie powiesz. Konkretne strony, komponenty czy funkcje włączasz do cache'owania za pomocą dyrektywy use cache.
Next.js prerenderuje statyczną powłokę HTML i serwuje ją natychmiast, a następnie streamuje dynamiczne części, gdy są już gotowe. Mieszanie statycznego i dynamicznego w obrębie jednego route'a — to właśnie Partial Prerendering (PPR), a w Next 16 jest to po prostu domyślne zachowanie Cache Components, a nie osobna flaga, którą się przełącza.
Model sprowadza się więc do dwóch pytań na każdy kawałek UI:
- Czy to może być takie samo dla wszystkich przez jakiś czas? → zcache'uj to, trafia do statycznej powłoki.
- Czy to jest per-request albo per-user? → zostaw dynamiczne, wstreamuje się w dziurę.
Włączanie
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;
Ta jedna flaga zastępuje stare experimental.ppr, experimental.useCache i experimental.dynamicIO. Jeśli korzystałeś z którejś z nich, przewodnik migracji do Next 16 opisuje ścieżkę przejścia.
use cache — trzy poziomy
Dyrektywa działa na poziomie pliku, komponentu lub funkcji:
// Cały plik zcache'owany
"use cache";
export default async function Page() {
// ...
}
// Zcache'owany pojedynczy komponent
export async function PriceList() {
"use cache";
const prices = await getPrices();
return <ul>{/* ... */}</ul>;
}
// Zcache'owana tylko funkcja pobierająca dane
export async function getProducts() {
"use cache";
return db.query.products.findMany();
}
Po poziom funkcji sięgam najczęściej: cache'uj kosztowny dostęp do danych, a komponenty wokół niego pozostaw dynamiczne.
cacheLife — jak długo trwa "jakiś czas"
cacheLife ustawia profil rewalidacji dla cache'owanego zakresu. Są nazwane profile, ale możesz też definiować własne:
import { unstable_cacheLife as cacheLife } from "next/cache";
export async function getBlogPosts() {
"use cache";
cacheLife("days"); // treść bloga, która aktualizuje się codziennie
return db.query.posts.findMany();
}
Wybierz profil odpowiadający temu, jak nieświeże dane mogą być. Teksty marketingowe? days. Katalog produktów? hours. Nie sięgaj po inwalidację on-demand, dopóki okno czasowe naprawdę nie jest w stanie wyrazić wymagania.
cacheTag + inwalidacja — oraz różnica między revalidate a update
Otaguj cache'owany wpis, żebyś mógł go precyzyjnie zinwalidować, gdy zmienią się dane leżące u jego podstaw:
import { unstable_cacheTag as cacheTag } from "next/cache";
export async function getProduct(id: string) {
"use cache";
cacheTag(`product-${id}`);
return db.query.products.findFirst({ where: eq(products.id, id) });
}
Następnie, gdy coś się zmieni, masz dwie funkcje inwalidacji i nie są one wymienne:
revalidateTag(tag)— używasz jej w Route Handlerach, webhookach albo gdziekolwiek. Oznacza tag jako nieświeży; kolejny request go odświeży.updateTag(tag)— wyłącznie w Server Actions. Stworzona do read-your-own-writes: natychmiast wygasza tag, a kolejny render czeka na świeże dane, więc użytkownik, który właśnie coś stworzył, widzi swoją zmianę, a nie nieświeżą kopię.
Reguła, którą stosuję: webhook z CMS-a woła revalidateTag; użytkownik edytujący własne dane w Server Action woła updateTag.
"use server";
import { updateTag } from "next/cache";
export async function updateProduct(id: string, data: FormData) {
await db.update(/* ... */);
updateTag(`product-${id}`); // ten użytkownik od razu widzi edycję
}
Pułapka, która cię ugryzie: cookies i headers
Cache'owany zakres nie może czytać danych z czasu requestu, takich jak cookies() czy headers() — to pozbawiłoby słowo "cache" sensu. Zamierzony wzorzec to odczytanie ich poza cache'owanym zakresem i przekazanie wartości jako argumenty:
// page.tsx (dynamiczny)
import { cookies } from "next/headers";
import { getDashboard } from "./data";
export default async function Page() {
const region = (await cookies()).get("region")?.value ?? "eu";
return <Dashboard promise={getDashboard(region)} />;
}
// data.ts
export async function getDashboard(region: string) {
"use cache";
cacheTag(`dashboard-${region}`);
// region przyszedł jako argument — cache'owalny per region
}
Jeśli naprawdę nie da się przerobić kodu tak, by przekazywać wartości do środka, jest use cache: private do cache'owania per-user oraz use cache: remote, gdy cache w pamięci nie wystarcza, a twoja platforma dostarcza dedykowany handler. Po te sięgaj na końcu.
Jak faktycznie strukturyzuję stronę
Wzorzec, który stał się moim domyślnym dla strony łączącej treść z personalizacją:
- Statyczna powłoka: header, hero, sekcje marketingowe — cache'owane, prerenderowane, serwowane z edge'a natychmiast.
- Dynamiczne dziury w
<Suspense>: "ostatnio oglądane", licznik koszyka, wszystko per-user — wstreamowane.
export default async function Page() {
return (
<>
<MarketingSections /> {/* "use cache" — w powłoce */}
<Suspense fallback={<CartSkeleton />}>
<CartSummary /> {/* dynamiczne — wstreamuje się */}
</Suspense>
</>
);
}
Odwiedzający od razu widzi kompletnie wyglądającą stronę, a spersonalizowane fragmenty doładowują się chwilę później. To cała obietnica PPR — a teraz jest domyślem, a nie flagą, którą trzeba uzasadniać.
Migracja ze starego świata
Jeśli przychodzisz z Next 15 lub wcześniejszego:
export const revalidate = 3600na segmencie → przenieś intencję do zakresuuse cachezcacheLife.unstable_cache(fn, keys, { tags })→ funkcja zuse cache+cacheTag.- konfiguracja route'a
experimental.ppr/experimental_ppr→ znika;cacheComponentsdaje ci PPR domyślnie.
Przewodnik "Migrating to Cache Components" w dokumentacji Next omawia mechaniczne części. Zmiana koncepcyjna to jednak to, co warto zinternalizować: przestałeś deklarować, co jest dynamiczne, a zacząłeś deklarować, co jest cache'owane.
Kiedy sobie odpuszczam
Cache Components błyszczy, gdy route miesza stabilną i per-user treść. Dla w pełni statycznej strony marketingowej to przerost formy — zwykły statyczny rendering jest prostszy. Dla w pełni dynamicznego dashboardu, gdzie nic nie da się współdzielić, i tak w większości zostawiasz rzeczy dynamiczne. Punkt idealny to ten zabałaganiony środek — który akurat jest miejscem, gdzie żyje większość prawdziwych stron klienckich.