Artikel

use cache in Next.js 16: Caching auf dem Server mit Cache Components

use cache ist nicht mehr Canary – mit Next.js 16 ist es als Teil der Cache Components stabil. Wie du es aktivierst, wie der Cache-Key entsteht, warum cookies() darin nicht funktioniert und wann du weiterhin unstable_cache brauchst.

Furkan Özay15. Juni 2025 · 10 Min. Lesezeit
czCZAY.DEV // EDITORIALDE
MODERN WEB & REACT

#React · 10 MIN LESEZEIT

FURKAN ÖZAYVOL. 2025 // #1707

Wichtig: Dieser Beitrag wurde aktualisiert

Als ich ihn zum ersten Mal geschrieben habe, war use cache in Next.js 15 nur im Canary-Release verfügbar, und ich habe geschrieben: „Nicht in Produktion verwenden." Das stimmt nicht mehr: Mit Next.js 16.0 ist es als Teil der Cache-Components-Funktion in die stabile Version übergegangen. Ich habe den Beitrag anhand der 16.x-Dokumentation komplett neu geschrieben – hier findest du sowohl die richtige Verwendung als auch die Stellen, an denen die meisten hängenbleiben.

Caching auf dem Server war in Next.js lange ein zersplittertes Thema: ein eigenes Verhalten für fetch-Aufrufe, eine eigene API für unstable_cache und React.cache für Wiederholungen innerhalb derselben Anfrage. use cache ersetzt diese drei durch eine einzige Direktive – du cachst die Ausgabe einer Funktion, einer Komponente oder einer ganzen Datei, und den Key erzeugt Next.js selbst.

Zuerst: Ohne Aktivierung läuft nichts

Das ist der am häufigsten übersehene Schritt. use cache zu schreiben reicht nicht, du musst die Funktion Cache Components aktivieren:

Beispiel: next.config.ts

TypeScript
import type { NextConfig } from "next";
 
const nextConfig: NextConfig = {
	cacheComponents: true,
};
 
export default nextConfig;

Wenn du die Direktive ohne diese Option schreibst, wird nichts gecacht und alles läuft stillschweigend einfach weiter – du bekommst auch keinen Fehler. Das ist das, was bei der Suche nach der Ursache für Langsamkeit am meisten Zeit kostet.

Wo schreibt man sie hin?

Es gibt drei Stellen, und an allen dreien muss die Funktion async sein:

Beispiel: Funktions-, Komponenten- und Dateiebene

TSX
// 1. Fonksiyon seviyesi
export async function getData() {
	"use cache";
	const res = await fetch("https://api.example.com/data");
	return res.json();
}
 
// 2. Bileşen seviyesi — prop'lar önbellek anahtarına girer
export async function Bookings({ type = "haircut" }: { type: string }) {
	"use cache";
	const res = await fetch(`https://api.example.com/bookings?type=${type}`);
	return <List items={await res.json()} />;
}
TSX
// 3. Dosya seviyesi — dosyadaki her export önbelleğe alınır
"use cache";
 
export async function getMonthlyTotals(accountId: string) {
	return db.orders.aggregate({ where: { accountId }, _sum: { amount: true } });
}
 
export async function getTopProducts() {
	return db.products.findMany({ orderBy: { sales: "desc" }, take: 10 });
}

Wenn du sie auf Dateiebene verwendest, fällt jeder Export darunter, einschließlich generateMetadata und generateStaticParams; alle müssen async sein. Wenn du ein Route-Segment komplett vorab generieren willst, musst du die Direktive an den Anfang jeder Datei setzen, die diese Route bildet (page, layout, Slots paralleler Routen) – jedes Segment ist ein eigener Cache-Eintrag.

Wie wird der Cache-Key erzeugt?

Der Key besteht aus vier Teilen:

  1. Build-ID – ändert sich bei jedem Build, also macht jedes Deployment den gesamten Cache ungültig
  2. Funktionsidentität – die Position der Funktion im Code und ein sicherer Hash ihrer Signatur
  3. Serialisierbare Argumente – bei Komponenten die Props, bei Funktionen die Parameter
  4. HMR-Hash – nur in der Entwicklung, verwirft den Cache bei Hot Reload

Die eigentliche Feinheit liegt nicht im vierten Punkt, sondern bei den Argumenten: Variablen, die du aus dem äußeren Scope verwendest, werden automatisch ebenfalls als Argumente gezählt und gehen in den Key ein.

Wichtig: Die Closure ist Teil des Keys

TSX
async function Component({ userId }: { userId: string }) {
	const getData = async (filter: string) => {
		"use cache";
		// Anahtar hem userId'yi (kapanıştan) hem filter'ı (argümandan) içerir
		const res = await fetch(
			`https://api.example.com/users/${userId}/data?filter=${filter}`
		);
		return res.json();
	};
 
	return getData("active");
}

Die gute Nachricht: Du zeigst nicht versehentlich die Daten eines Nutzers einem anderen an. Die schlechte: Du kannst unbemerkt den Key aufsplitten und damit die Cache-Trefferquote senken. Denk daran, dass jede äußere Variable, die du darin verwendest, den Key teilt.

Wie lange bleibt es frisch: cacheLife

Wenn du cacheLife nicht aufrufst, gilt das Profil default:

  • stale: 5 Minuten (clientseitig)
  • revalidate: 15 Minuten (serverseitig)
  • expire: läuft zeitlich nie ab

Tipp: Schreib die Lebensdauer explizit hin

TSX
import { cacheLife } from "next/cache";
 
export async function getOrderSummary(accountId: string) {
	"use cache";
	cacheLife("hours");
 
	const orders = await getOrders(accountId);
	return { orders };
}

Die Empfehlung der Dokumentation ist eindeutig: Schreibe in jedem use cache-Scope cacheLife explizit hin. Sonst ist das Verhalten an der Aufrufstelle nicht sichtbar und hängt vom Standard oder vom umgebenden Cache ab.

Eine Warnung: Wenn du in ein use cache ohne angegebenes cacheLife ein weiteres, kurzlebiges use cache setzt, schlägt der Build mit einem Fehler fehl. Bei verschachtelten Caches musst du die Lebensdauern bewusst wählen.

Taggen und manuell leeren: cacheTag

Zeitbasierte Aktualisierung ist nicht immer die richtige Antwort. Wenn du den Cache sofort leeren willst, sobald sich der Inhalt ändert, nutze Tags:

Beispiel: cacheTag + updateTag

TSX
// lib/products.ts
import { cacheTag } from "next/cache";
 
export async function getProducts() {
	"use cache";
	cacheTag("products");
	const res = await fetch("https://api.example.com/products");
	return res.json();
}
TSX
// app/actions.ts
"use server";
 
import { updateTag } from "next/cache";
 
export async function updateProduct() {
	await db.products.update(/* ... */);
	updateTag("products"); // "products" etiketli tüm girdiler düşer
}

In der Praxis nutzt man beides zusammen: Ein selten geänderter Blogbeitrag bekommt ein langes cacheLife plus cacheTag und wird nur beim Speichern geleert; eine Liste, die sich im Tagesverlauf ständig ändert, aktualisiert sich dagegen mit einem kurzen Profil von selbst.

Die häufigste Stolperfalle: cookies() funktioniert darin nicht

In einem gecachten Scope sind cookies(), headers() und searchParams nicht verwendbar. Die Einschränkung folgt außerdem dem Call Stack: Wenn eine Hilfsfunktion, die du von innen aufrufst, eines davon liest, knallt es auch dort (next-request-in-use-cache).

Achtung: Die heimtückische Seite

Bei einer dynamisch gerenderten Route tritt dieser Fehler nicht beim Build, sondern zur Laufzeit auf. Das heißt: next build läuft problemlos durch, und unter next start bekommst du einen Fehler. In der CI grün, in Produktion rot.

Das richtige Muster: Lies die Request-Daten außerhalb des Cache-Scopes und gib sie als Argument hinein.

Erklärung: Falsch / richtig

TSX
// YANLIŞ — çalışma anında patlar
async function getCart() {
	"use cache";
	const store = await cookies();
	return db.cart.find({ id: store.get("cart")?.value });
}
 
// DOĞRU — okuma dışarıda, argüman içeride
export default async function Page() {
	const store = await cookies();
	const cartId = store.get("cart")?.value;
	return <Cart cartId={cartId} />;
}
 
async function Cart({ cartId }: { cartId?: string }) {
	"use cache";
	return <CartView data={await db.cart.find({ id: cartId })} />;
}

Für Fälle, in denen es wirklich unvermeidbar ist, gibt es use cache: private, aber die Dokumentation kennzeichnet es als „sehr selten" – gedacht für Compliance-Vorgaben oder wenn du den Code nicht umstrukturieren kannst.

Grenzen der Serialisierung

Argumente und Rückgabewerte müssen serialisierbar sein.

Gültig: primitive Typen, einfache Objekte, Arrays, Date, Map, Set, TypedArray, ArrayBuffer. Der Rückgabewert darf außerdem JSX sein.

Ungültig: Klasseninstanzen, Funktionen, Symbol, WeakMap/WeakSet und – leicht zu vergessen – URL-Instanzen.

Der rettende Detailpunkt: Du kannst einen Wert durchreichen und verwenden, ohne hineinzuschauen. So funktionieren children und Server Actions.

Tipp: Das Pass-through-Muster

TSX
async function CachedWrapper({ children }: { children: ReactNode }) {
	"use cache";
	// children okunmuyor, sadece geçiriliyor — anahtarı etkilemiyor
	return (
		<div className="wrapper">
			<header>Önbelleğe alınmış başlık</header>
			{children}
		</div>
	);
}
 
export default function Page() {
	return (
		<CachedWrapper>
			<DynamicComponent /> {/* önbelleğe alınmıyor */}
		</CachedWrapper>
	);
}

Damit kannst du die statische Hülle der Seite cachen und den dynamischen Teil hineinsetzen. Wenn du use cache in ein layout schreibst, werden die darunterliegenden children nicht mitgecacht.

YouTube

Zu diesem Thema gibt es auch ein Video-Walkthrough.

Zum Kanal →

Wo wird es gespeichert? Die Hosting-Art ändert das Ergebnis

Der Standard-Handler ist ein In-Memory-LRU. Das verhält sich je nachdem, wo du deployst, ganz unterschiedlich:

UmgebungVerhalten zur Laufzeit
ServerlessEinträge bleiben zwischen Anfragen meist nicht erhalten; jede Anfrage kann auf einer anderen Instanz landen. Caching zur Build-Zeit funktioniert normal.
Eigener ServerEinträge bleiben zwischen Anfragen erhalten. Die Größe stellst du mit cacheMaxMemorySize ein.

Warnung: Ein häufiger Fehlschluss

Die meisten Beschwerden der Art „Ich habe use cache gesetzt, aber die Datenbanklast ist nicht gesunken" lassen sich mit dieser Tabelle erklären. Auf Serverless ist der In-Memory-Cache nicht persistent. Wenn du wirklich einen geteilten Cache willst, musst du mit use cache: remote einen separaten Handler (Redis, KV) anbinden – auch das bringt Netzwerklatenz und Plattformkosten mit sich.

Außerdem wird keine Cache-Direktive in ein neues Deployment übernommen, weil der Key die Build-ID enthält. Für Daten, die über Deployments hinweg erhalten bleiben sollen, brauchst du weiterhin unstable_cache oder den fetch-Cache. use cache ersetzt nicht alles.

Wenn der Build nach 50 Sekunden hängt

Wenn du diesen Fehler siehst, ist die Ursache fast immer dieselbe:

Filling a cache during prerender timed out, likely because request-specific arguments such as params, searchParams, cookies() or uncached data were used inside "use cache".

Der gecachte Scope wartet auf ein Promise, das sich beim Build nicht auflösen lässt. Typisch sind drei Wege: ein solches Promise als Prop übergeben, über eine Closure darauf zugreifen oder es über eine geteilte Map beziehen. Die Lösung ist immer dieselbe: den Wert außerhalb mit await auflösen und einfache Daten hineingeben.

cookies() direkt im Inneren aufzurufen führt dagegen nicht zu einem Timeout, sondern sofort zu einem Fehler – verwechsle die beiden nicht.

Zwei weitere Verhaltensweisen, die du kennen solltest

Bei aktivem Draft Mode werden alle gecachten Funktionen bei jeder Anfrage neu ausgeführt und das Ergebnis wird nicht gespeichert. In der Vorschau besteht kein Risiko, veraltete Inhalte zu sehen; dafür musst du keinen zusätzlichen Code schreiben.

React.cache ist isoliert. Ein Wert, den du außerhalb der use cache-Grenze mit React.cache speicherst, ist von innen nicht sichtbar. Der einzige Weg, Daten hineinzubringen, ist ein Argument.

Wann du es nicht verwenden solltest

  • Beim statischen Export (output: "export") wird es nicht unterstützt. Ein Node.js-Server und Docker werden unterstützt.
  • Bei nutzerspezifischen Daten, die sich bei jeder Anfrage ändern – du müsstest sie aus dem Scope herausholen und als Argument übergeben, was den Cache meist sinnlos macht.
  • Bei teuren Berechnungen, die über Deployments hinweg leben sollen; der Build-ID-Key setzt sie zurück.
  • Wenn die Aktualität der Daten auf die Sekunde ankommt: Der Client-Router erzwingt eine Stale-Zeit von mindestens 30 Sekunden, unabhängig von der Konfiguration.

Debugging

Tipp: Ausführliches Logging

Bash
NEXT_PRIVATE_DEBUG_CACHE=1 npm run dev
# ya da üretim için
NEXT_PRIVATE_DEBUG_CACHE=1 npm run start

In der Entwicklung erscheinen console.log-Ausgaben aus gecachten Funktionen mit dem Präfix Cache – der schnellste Weg zu erkennen, welcher Aufruf aus dem Cache kam.

Zusammenfassung

Zusammenfassung: Kurzfassung

  • use cache ist in Next.js 16.0 stabil; aktiviert wird es mit cacheComponents: true
  • Funktioniert auf Funktions-, Komponenten-, Datei- und Route-Segment-Ebene; alles muss async sein
  • Key: Build-ID + Funktionsidentität + Argumente + aus der Closure erfasste Variablen
  • Lebensdauer mit cacheLife, manuelles Leeren mit cacheTag + updateTag
  • cookies(), headers(), searchParams sind darin verboten – außerhalb lesen, als Argument übergeben
  • Standard-Speicher ist ein In-Memory-LRU; auf Serverless bleibt er zwischen Anfragen nicht erhalten
  • Wenn du Persistenz über Deployments hinweg brauchst, weiterhin unstable_cache oder der fetch-Cache

In der ersten Fassung dieses Beitrags gab es einen Abschnitt „Performance-Test“ mit Zahlen wie 78 % Beschleunigung. Ich habe ihn entfernt, weil diese Zahlen nicht aus einer echten Messung stammten, sondern aus einem erfundenen setTimeout-Aufbau und dir nichts nützten, um den Gewinn in deinem Projekt abzuschätzen. Der Gewinn hängt ganz davon ab, was du cachst: Eine Datenbankabfrage, die 200 ms dauert, zu cachen ist nicht dasselbe wie eine Berechnung, die 2 ms dauert. Miss selbst mit NEXT_PRIVATE_DEBUG_CACHE=1.

Hinweis: Quelle

Dieser Beitrag basiert auf der offiziellen Next.js-16.x-Dokumentation (die use-cache-Direktive). Versionsverlauf: v15.0.0 führte sie experimentell ein, v16.0.0 machte sie zusammen mit Cache Components stabil.

Interaktive Roadmap & Tool

Als Nächstes lesen