use cache w Next.js: klucz, czas życia i updateTag

Jak działa use cache w Next.js, na żywym demo: klucz cache tworzą argumenty, cacheLife ustala czas życia wpisu, a updateTag czyści wpisy jednego tagu.

Czas czytania: 6 min czytaniaOpublikowano:

Wpisz "use cache" na początku funkcji asynchronicznej, a Next.js zapisze jej wynik pod kluczem zbudowanym z builda, funkcji i jej argumentów: wywołania z tymi samymi argumentami dzielą jeden wpis, inne argumenty dostają własny. Wpis żyje tak długo, jak pozwala profil cacheLife (otwiera się w nowej karcie) (bez wywołania profil default odświeża go po 15 minutach i nigdy nie wygasza po czasie), a Server Action, która wywoła updateTag z tagiem wpisu z cacheTag, czyści tylko wpisy z tym tagiem. Wszystkie trzy rzeczy pokazujemy na jednym demo na Next.js 16.4.0, a jego test w Playwright sprawdza każde twierdzenie o demo z tego tekstu. We własnej aplikacji używaj 16.4.1 lub nowszej wersji: naprawia krytyczne RCE w funkcjach use cache, jedną z poprawek RCE w React i Next.js z października 2026.

Oba wiersze Gdańska mają jedno id, Kraków ma swoje, Reload ich nie zmienia, a Clear przy Gdańsku zmienia tylko Gdańsk. · Next.js 16.4.0Otwórz demoZobacz kod

Jak działa use cache: kiedy dwa wywołania dzielą wpis

Dokumentacja use cache (otwiera się w nowej karcie) wymienia, co po serializacji trafia do klucza wpisu:

Składnik kluczaCo to znaczy w praktyce
Build ID (albo deploymentId)Nowy deploy zaczyna każdy wpis od nowa.
ID funkcjiSkrót położenia i sygnatury funkcji: te same argumenty w innej funkcji z cache to inny wpis.
Serializowalne argumentyKażdy inny argument (albo zestaw propsów) to osobny wpis.
Zmienne z domknięciaWartości czytane z zewnętrznego zakresu trafiają do klucza jak argumenty.
Skrót odświeżenia HMRTylko w trybie deweloperskim: edycja kodu unieważnia cache.

Demo ma jedną funkcję z cache. Tworzy id i znacznik czasu w swoim ciele, więc nowe id oznacza, że ciało wykonało się ponownie:

app/demos/nextjs-use-cache/get-stamp.ts
import { cacheLife, cacheTag } from "next/cache"; export type City = "gdansk" | "krakow"; // One cached function. The entry's key is built from its argument, so getStamp("gdansk") and// getStamp("krakow") are two entries, and every call with "gdansk" shares one.export async function getStamp(city: City) {  "use cache";  cacheLife("hours"); // revalidate after 1 hour, expire after 1 day  cacheTag(`city-${city}`); // updateTag("city-gdansk") clears this city's entry only   // Produced inside the cached body: a new id means the body ran again.  return { id: crypto.randomUUID().slice(0, 8), at: new Date().toISOString() };}
Z labu, commit 6ad61f5

Strona wywołuje ją trzy razy: dwa razy z "gdansk" i raz z "krakow". Oba wiersze Gdańska pokazują to samo id i czas, bo ten sam argument zwraca ten sam wpis. Kraków ma inne id: miasta mają osobne wpisy. Wywołania nie różnią się niczym innym, więc o wpisie decyduje sam argument.

Argumenty muszą być serializowalne: typy proste, zwykłe obiekty, tablice, Date, Map i Set przejdą, instancje klas i obiekty URL nie, a funkcje i JSX można tylko przekazać dalej (otwiera się w nowej karcie) bez zaglądania do nich. Przekazuj id rekordu, a nie obiekt klienta, a klucz zostanie mały i stabilny.

Jak długo żyje wpis use cache

Przeładuj demo: id i czasy z cache zostają te same, a wiersz „Not cached”, który czeka na żądanie przez connection(), zmienia się przy każdym wczytaniu. Z wyłączonym JavaScriptem przeglądarka pokazuje HTML, który serwer wysłał najpierw: id z cache już w nim są, a wiersz bez cache pokazuje tylko placeholder. Zgadza się to z dokumentacją: przy Cache Components prerender wypełnia wpis (otwiera się w nowej karcie), a jego wynik staje się częścią statycznej powłoki strony.

Jak długo tam zostaje, ustala profil cacheLife. Wbudowane profile (otwiera się w nowej karcie) według dokumentacji z 19 października 2026:

Profilstale (przeglądarka)revalidate (serwer)expire
default (bez cacheLife)5 minut15 minutnigdy
seconds30 sekund1 sekunda1 minuta
minutes5 minut1 minuta1 godzina
hours (w demo)5 minut1 godzina1 dzień
days5 minut1 dzień1 tydzień
weeks5 minut1 tydzień30 dni
max5 minut30 dni1 rok

revalidate to moment, po którym kolejne żądanie dostaje kopię z cache, a świeża powstaje w tle; expire to czas, przez jaki wpis może nie być pobierany, zanim następne żądanie poczeka na świeży render. stale dotyczy przeglądarki: router po stronie klienta trzyma wynik tak długo, ale co najmniej 30 sekund. To ten sam cache, do którego trafiają prefetchowane strony; tę część zmierzył nasz benchmark prefetchu w Next.js.

Krótki czas życia zmienia, gdzie wpis może trafić. Według progów prerenderu w cacheLife (otwiera się w nowej karcie) expire poniżej 5 minut albo revalidate równe 0 robi z niego dynamiczną dziurę renderowaną w czasie żądania, więc z gotowych profili tylko seconds nigdy nie trafia do statycznej powłoki. Taki komponent owiń w <Suspense>.

Jak wyczyścić wpis use cache przez updateTag

updateTag (otwiera się w nowej karcie) można wywołać tylko w Server Action. Od razu wygasza każdy wpis z danym tagiem, a następne żądanie czeka na świeże dane, zamiast dostać starą kopię. Akcja w demo bierze miasto z przycisku i czyści tag tego miasta:

app/demos/nextjs-use-cache/actions.ts
"use server"; import { updateTag } from "next/cache"; import type { City } from "./get-stamp"; const cities: City[] = ["gdansk", "krakow"]; // A Server Action: updateTag works only here. It expires every entry tagged with this city, so// the next render runs getStamp for that city again and reuses the other city's entry.export async function clearCity(formData: FormData) {  const city = formData.get("city");  if (!cities.includes(city as City)) return;  updateTag(`city-${city}`);}
Z labu, commit 6ad61f5

Kliknij Clear przy Gdańsku: bez przeładowania Gdańsk dostaje nowe id (w obu wierszach, bo dzielą wpis), a Kraków zachowuje swoje. Zasięg czyszczenia wyznacza jeden tag na miasto: updateTag wygasza każdy wpis z tym tagiem, więc wspólny tag obu miast wyczyściłby też Kraków.

Sama strona to zwykłe Server Components: wiersze z cache, wiersz bez cache w <Suspense> i formularz dla każdego miasta, który wysyła akcję:

app/demos/nextjs-use-cache/page.tsx
import { connection } from "next/server";import { Suspense } from "react"; import { clearCity } from "./actions";import { getStamp, type City } from "./get-stamp";import styles from "./stamps.module.css"; export default function Page() {  return (    <>      <div className={styles.rows}>        <CachedRow city="gdansk" name="Gdansk" clearable />        <CachedRow city="gdansk" name="Gdansk again" />        <CachedRow city="krakow" name="Krakow" clearable />        <Suspense fallback={<Row name="Not cached" call="connection()" pending />}>          <UncachedRow />        </Suspense>      </div>      <p className={styles.reload}>        <a href="/demos/nextjs-use-cache">Reload</a>, then clear one city.      </p>    </>  );} // Same function, same argument, same entry: both Gdansk rows show one id.async function CachedRow({ city, name, clearable }: { city: City; name: string; clearable?: boolean }) {  const stamp = await getStamp(city);  return <Row name={name} call={`getStamp("${city}")`} stamp={stamp} clear={clearable ? city : undefined} />;} // No cache: connection() waits for a request, so this runs on every load.async function UncachedRow() {  await connection();  const stamp = { id: crypto.randomUUID().slice(0, 8), at: new Date().toISOString() };  return <Row name="Not cached" call="connection()" stamp={stamp} />;} type RowProps = { name: string; call: string; stamp?: { id: string; at: string }; clear?: City; pending?: boolean }; function Row({ name, call, stamp, clear, pending }: RowProps) {  return (    <section className={styles.row} aria-label={name}>      <div>        <h2>{name}</h2>        <code>{call}</code>      </div>      <p className={styles.stamp}>        {stamp ? (          <>            <output>{stamp.id}</output>            <time dateTime={stamp.at}>{stamp.at.slice(11, 19)} UTC</time>          </>        ) : (          pending && <span>rendering…</span>        )}      </p>      {clear && (        <form action={clearCity}>          <button name="city" value={clear}>            Clear          </button>        </form>      )}    </section>  );}
Z labu, commit 6ad61f5

W webhooku albo Route Handlerze, gdzie updateTag rzuca błąd, użyj revalidateTag(tag, "max") (otwiera się w nowej karcie): serwuje kopię z cache, a świeża powstaje w tle.

Jak dodać use cache do własnej aplikacji Next.js

  1. Włącz Cache Components

    Ustaw cacheComponents: true w next.config.ts. use cache należy do Cache Components i działa tylko z tą opcją.

  2. Cache'uj funkcję, nie żądanie

    Wpisz "use cache" w pierwszej linii funkcji asynchronicznej, która ładuje wspólne dane. Cookies, nagłówki i parametry wyszukiwania czytaj poza nią i przekazuj potrzebne wartości jako argumenty.

  3. Ustaw cacheLife wprost

    Wywołaj cacheLife("hours") (albo profil pasujący do tego, jak często zmieniają się dane) w każdej funkcji z cache, żeby czas życia był widoczny tam, gdzie ją zdefiniowano, zamiast zależeć od default.

  4. Otaguj wpis tym, co go zmienia

    Wywołaj cacheTag (otwiera się w nowej karcie) z jednym tagiem na rekord, a gdy rekord pokazuje też lista, dodaj drugi tag dla listy: jeden wpis może mieć kilka tagów.

  5. Czyść tag tam, gdzie zmieniają się dane

    Po zapisie w Server Action wywołaj updateTag z tagiem rekordu. Z webhooka albo Route Handlera wywołaj revalidateTag(tag, "max").

  6. Sprawdź to na buildzie produkcyjnym

    Uruchom next build i next start, a potem przeładuj stronę i wyczyść tag jak w demo. W trybie deweloperskim skrót HMR jest częścią klucza, więc każda edycja czyści cache.

Konfiguracja to jedna linia:

next.config.ts
const nextConfig = { cacheComponents: true };

Co psuje use cache

cookies() i headers() w funkcji z cache

Funkcja z cache nie może czytać cookies(), headers() ani searchParams, a zakaz obejmuje cały stos wywołań: helper, który czyta jedną z tych wartości, kończy się tym samym błędem next-request-in-use-cache (otwiera się w nowej karcie). Na trasie renderowanej dynamicznie błąd wychodzi dopiero przy jej uruchomieniu, więc build może przejść, a next start się wyłożyć. Odczytaj wartość w komponencie i przekaż ją jako argument: wtedy trafi też do klucza.

Gdzie pasuje use cache: private

Gdy wyniesienie odczytu żądania na zewnątrz jest niepraktyczne, 'use cache: private' (otwiera się w nowej karcie) pozwala na cookies(), headers() i searchParams w zakresie z cache. To inny cache: na produkcji pasujące wywołania w obrębie jednego żądania dzielą wynik, ale nic nie zostaje zapisane na serwerze między żądaniami, a funkcja działa w czasie żądania, poza statyczną powłoką. Przeglądarka może trzymać wynik przez czas stale. connection() jest zakazane w obu dyrektywach.

Hosting serverless i nowe deploye

Domyślny handler trzyma wpisy w pamięci. Na serverless dokumentacja mówi, że wpisy zwykle nie przetrwają między żądaniami (otwiera się w nowej karcie), bo każde może trafić na inną instancję, a cache z czasu builda działa normalnie. Na własnym serwerze jeden proces je trzyma, a limit ustawia cacheMaxMemorySize. Jeśli na serverless liczą się wpisy tworzone w czasie żądania, dokumentacja wskazuje 'use cache: remote' i handler wspólny dla instancji. A że build ID jest w kluczu, każdy deploy zaczyna z pustym cache.

Brak cacheLife w zagnieżdżonym cache

Funkcja z use cache bez własnego cacheLife dostaje profil default, a wewnętrzny cache z krótszym czasem życia może go skrócić, jak opisują zasady zagnieżdżania w cacheLife (otwiera się w nowej karcie). Zagnieżdżenie cache seconds w zewnętrznym bez cacheLife kończy build błędem podczas prerenderu. Jawne cacheLife w każdej funkcji z cache zapobiega obu sytuacjom.

Z czego składa się klucz cache w use cache?

Z zserializowanych danych wejściowych wpisu: build ID (albo deploymentId, jeśli go ustawisz), skrótu położenia i sygnatury funkcji oraz jej serializowalnych argumentów lub propsów. Zmienne, które funkcja czyta z zewnętrznego zakresu, też trafiają do klucza. W trybie deweloperskim dochodzi skrót odświeżenia HMR.

Czy use cache działa na serverless i na Vercel?

Cache z czasu builda działa tam normalnie, więc wynik prerenderowany do statycznej powłoki jest serwowany tak, jak go zbudowano. Wpisy tworzone w czasie żądania trafiają do domyślnego cache w pamięci, który na serverless zwykle nie przetrwa między żądaniami, bo każde może trafić na inną instancję. Dla cache wspólnego dla instancji dokumentacja Next.js wskazuje 'use cache: remote'.

Czym różni się use cache od unstable_cache?

Wpisy use cache mają w kluczu build ID, więc żaden nie przechodzi na nowy deploy. Dla danych, które muszą przetrwać deploy, dokumentacja Next.js poleca unstable_cache (dla funkcji innych niż fetch) albo cache fetch.

Czym różni się cache z Reacta od use cache?

cache z Reacta zapamiętuje wynik funkcji na czas jednego żądania na serwerze i jest unieważniany przy każdym nowym. use cache trzyma wynik pod swoim kluczem między żądaniami, aż cacheLife albo tag zakończy wpis. Wartości zapisane przez cache z Reacta poza funkcją z use cache nie są w niej widoczne, więc dane przekazuj jako argumenty.

Jak włączyć use cache w Next.js 16?

Ustaw cacheComponents: true w next.config.ts. use cache należy do Cache Components i działa tylko z tą opcją.

Dalsza lektura

Next.js: use cache (otwiera się w nowej karcie)Klucze cache, serializacja, cache w czasie działania zależnie od hostingu i rewalidacja.nextjs.org
Next.js: cacheLife (otwiera się w nowej karcie)Gotowe profile z czasami stale, revalidate i expire oraz progi prerenderu.nextjs.org
Next.js: updateTag (otwiera się w nowej karcie)Czyszczenie tagu z Server Action i różnice względem revalidateTag.nextjs.org
Next.js: use cache: private (otwiera się w nowej karcie)Cache funkcji, które czytają cookies lub nagłówki, i to, czego nie zapisuje.nextjs.org
Next.js: Remote Code Execution in use cache functions (otwiera się w nowej karcie)Komunikat z 14 października 2026: wersje od 16.0.0 do 16.4.0, poprawka w 16.4.1.github.com
WydajnośćNext.js prefetch zmierzony: Partial Prefetching w 16.3Partial Prefetching w Next.js 16.3 zmniejszył w naszym benchmarku prefetch nawet o 93%, ale treść adresu przychodzi po kliknięciu. Kiedy co się opłaca.

Napisz, co budujemy.

Odpowiadamy w ciągu jednego dnia roboczego.