Warum Next.js das Caching komplett neu gedacht hat
Hand aufs Herz: Wer in den letzten zwei Jahren mit dem Next.js App Router gearbeitet hat, kennt dieses nagende Gefühl. Man schreibt eine fetch-Anfrage, deployed die App — und wundert sich dann, warum bestimmte Daten gecacht werden und andere eben nicht. Das implizite Caching-Modell des App Routers war für viele von uns eine der größten Frustrationsquellen überhaupt. Mal war es zu aggressiv, mal komplett unvorhersehbar.
Und die Dokumentation? Die hat einen auch nicht immer weitergebracht.
Mit Next.js 16 hat Vercel dieses Problem grundlegend angegangen. Die Antwort heißt Cache Components — ein neues, vollständig opt-in-basiertes Caching-Modell, das auf der use cache-Direktive aufbaut. Statt impliziter Magie bekommt man jetzt tatsächlich explizite Kontrolle. Klingt zu schön, um wahr zu sein? Schauen wir uns das neue System im Detail an — mit praktischen Beispielen und konkreten Migrationsstrategien.
Was sind Cache Components eigentlich?
Cache Components ist ein Feature in Next.js 16, das man gezielt aktivieren muss. Im Kern ermöglicht es das Mischen von statischem, gecachtem und dynamischem Inhalt innerhalb einer einzelnen Route. Das klingt erstmal ziemlich abstrakt, also machen wir es konkreter.
Traditionell musste man sich bei serverseitig gerenderten Anwendungen zwischen zwei Extremen entscheiden:
- Statische Seiten: Schnell ausgeliefert, aber potentiell veraltet
- Dynamische Seiten: Immer aktuell, aber langsamer und ressourcenintensiver
Cache Components löst diesen Zielkonflikt, indem Routen in eine statische HTML-Hülle (Static Shell) vorgerendert werden. Diese Hülle wird sofort an den Browser gesendet, während dynamische Inhalte nachgeladen und eingefügt werden, sobald sie bereitstehen. Das Konzept dahinter nennt sich Partial Prerendering (PPR) — und es ist das neue Standardverhalten, wenn Cache Components aktiviert ist.
Ehrlich gesagt, das ist ziemlich elegant.
Cache Components aktivieren
Die Aktivierung ist denkbar unkompliziert. Einfach die Option cacheComponents in der Next.js-Konfigurationsdatei hinzufügen:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
Ab diesem Moment ändert sich das Rendering-Verhalten der gesamten Anwendung: Alle Seiten sind standardmäßig dynamisch, und Caching wird explizit über die use cache-Direktive gesteuert. Ein einziger Boolean — und das gesamte Caching-Modell kippt.
Wie das Rendering mit Cache Components funktioniert
Das Rendering-Modell lässt sich in drei Kategorien unterteilen, die auf einer einzelnen Seite koexistieren können. Und genau das macht es so mächtig.
1. Automatisch vorgerenderte Inhalte
Komponenten, die ausschließlich auf synchrone Operationen zugreifen — Dateisystem-Reads, Modul-Imports oder reine Berechnungen — werden automatisch in die statische Hülle aufgenommen. Hier braucht man kein use cache, das passiert einfach von selbst.
// Diese Komponente wird automatisch vorgerendert
import fs from 'node:fs'
export default async function KonfigurationsSeite() {
const config = fs.readFileSync('./config.json', 'utf-8')
const daten = JSON.parse(config)
return (
<div>
<h1>{daten.appName}</h1>
<p>Version: {daten.version}</p>
</div>
)
}
2. Gecachte dynamische Inhalte mit use cache
Wenn Komponenten auf externe Datenquellen zugreifen, die sich nicht bei jedem Request ändern, kann man die use cache-Direktive verwenden. Das Ergebnis wird gecacht und in die statische Hülle eingebunden:
import { cacheLife } from 'next/cache'
async function ProduktListe() {
'use cache'
cacheLife('hours')
const produkte = await fetch('https://api.example.com/produkte')
const daten = await produkte.json()
return (
<section>
<h2>Unsere Produkte</h2>
<ul>
{daten.map((p: any) => (
<li key={p.id}>{p.name} – {p.preis} €</li>
))}
</ul>
</section>
)
}
3. Dynamische Inhalte zur Laufzeit (Streaming)
Inhalte, die auf Request-spezifische Daten angewiesen sind — Cookies, Headers oder Suchparameter — müssen in eine <Suspense>-Boundary eingewickelt werden. Das kennt man ja schon:
import { cookies } from 'next/headers'
import { Suspense } from 'react'
async function BenutzerBereich() {
const cookieStore = await cookies()
const session = cookieStore.get('session')?.value
return (
<aside>
<p>Angemeldet als: {session ? 'Ja' : 'Nein'}</p>
</aside>
)
}
export default function Seite() {
return (
<>
<h1>Willkommen</h1>
<Suspense fallback={<p>Lade Benutzerbereich...</p>}>
<BenutzerBereich />
</Suspense>
</>
)
}
Der Fallback (<p>Lade Benutzerbereich...</p>) wird Teil der statischen Hülle, während der tatsächliche Inhalt per Streaming nachgeliefert wird. Der Benutzer sieht also sofort etwas — und nicht nur einen leeren Bildschirm.
Die use cache-Direktive im Detail
So, jetzt wird's spannend. Die use cache-Direktive ist das Herzstück des neuen Caching-Modells. Sie kann an drei Stellen eingesetzt werden:
- Dateiebene: Am Anfang der Datei — alle Exporte werden gecacht
- Komponentenebene: Am Anfang einer async Komponente
- Funktionsebene: Am Anfang einer async Funktion
// Dateiebene — alle Exporte gecacht
'use cache'
export default async function Seite() {
// ...
}
// Komponentenebene
export async function MeineKomponente() {
'use cache'
return <></>
}
// Funktionsebene
export async function holeDaten() {
'use cache'
const daten = await fetch('/api/daten')
return daten
}
Diese Flexibilität ist wirklich praktisch. Man kann ganz gezielt entscheiden, auf welcher Ebene Caching Sinn ergibt — statt pauschal eine ganze Route zu cachen oder eben gar nicht.
Wie Cache-Schlüssel erzeugt werden
Ein entscheidender Aspekt von use cache ist die automatische Erzeugung von Cache-Schlüsseln. Die setzen sich zusammen aus:
- Build-ID: Eindeutig pro Build — ein neuer Build invalidiert alle Cache-Einträge
- Funktions-ID: Ein Hash basierend auf Position und Signatur der Funktion im Code
- Serialisierbare Argumente: Props (bei Komponenten) oder Funktionsargumente
- HMR-Hash (nur in der Entwicklung): Invalidiert den Cache bei Hot Module Replacement
Variablen aus dem äußeren Scope werden dabei automatisch als Teil des Cache-Schlüssels erfasst:
async function BenutzerDaten({ userId }: { userId: string }) {
const holeDaten = async (filter: string) => {
'use cache'
// Cache-Schlüssel beinhaltet userId (aus Closure) und filter (Argument)
return fetch(`/api/benutzer/${userId}/daten?filter=${filter}`)
}
return holeDaten('aktiv')
}
Verschiedene Kombinationen von userId und filter erzeugen also separate Cache-Einträge. Das Schöne daran: Man muss keine Cache-Keys manuell definieren. Das Framework übernimmt das.
Serialisierungsregeln
Nicht alles kann gecacht werden — das wäre ja auch zu einfach. Argumente und Rückgabewerte müssen serialisierbar sein:
Unterstützt als Argumente:
- Primitive:
string,number,boolean,null,undefined - Einfache Objekte:
{ schlüssel: wert } - Arrays, Dates, Maps, Sets, TypedArrays
Nicht unterstützt:
- Klasseninstanzen
- Funktionen (außer als Pass-Through)
- Symbols, WeakMaps, WeakSets
- URL-Instanzen
Cache-Lebensdauer mit cacheLife steuern
Die Funktion cacheLife aus next/cache definiert, wie lange gecachte Daten gültig bleiben. Hier gibt es vordefinierte Profile und die Möglichkeit, eigene zu erstellen.
Vordefinierte Profile
Next.js 16 liefert folgende Profile mit:
| Profil | stale (Client) | revalidate (Server) | expire |
|---|---|---|---|
seconds | 30 Sekunden | 1 Sekunde | 1 Minute |
minutes | 5 Minuten | 1 Minute | 1 Stunde |
hours | 5 Minuten | 1 Stunde | 1 Tag |
days | 5 Minuten | 1 Tag | 1 Woche |
weeks | 5 Minuten | 1 Woche | 1 Monat |
max | 5 Minuten | 1 Monat | Unbegrenzt |
Die drei Timing-Eigenschaften im Detail:
- stale: Wie lange der Client die gecachten Daten verwenden darf, ohne den Server zu kontaktieren
- revalidate: Nach dieser Zeit löst der nächste Request eine Hintergrund-Aktualisierung aus
- expire: Maximale Lebensdauer, bevor der Server den Inhalt zwingend neu generiert
import { cacheLife } from 'next/cache'
export default async function BlogSeite() {
'use cache'
cacheLife('hours') // Stündliche Revalidierung
const beitraege = await fetch('https://api.example.com/blog')
const daten = await beitraege.json()
return (
<section>
{daten.map((b: any) => (
<article key={b.id}>
<h2>{b.titel}</h2>
<p>{b.auszug}</p>
</article>
))}
</section>
)
}
Benutzerdefinierte Profile
Für spezifische Anforderungen kann man eigene Profile erstellen — entweder inline oder in der next.config.ts. Gerade bei größeren Projekten lohnt sich die zentrale Konfiguration:
// Inline: Benutzerdefinierte Cache-Konfiguration
import { cacheLife } from 'next/cache'
async function ProduktDetails({ id }: { id: string }) {
'use cache'
cacheLife({
stale: 3600, // 1 Stunde stale am Client
revalidate: 7200, // Alle 2 Stunden revalidieren
expire: 86400, // Nach 1 Tag ablaufen
})
const produkt = await fetch(`https://api.example.com/produkte/${id}`)
return produkt.json()
}
// next.config.ts: Globale benutzerdefinierte Profile
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
cacheLife: {
produktDaten: {
stale: 300, // 5 Minuten
revalidate: 1800, // 30 Minuten
expire: 86400, // 1 Tag
},
seltenAktualisiert: {
stale: 600,
revalidate: 604800, // 1 Woche
expire: 2592000, // 30 Tage
},
},
}
export default nextConfig
Danach kann man das benutzerdefinierte Profil ganz normal verwenden:
async function ProduktListe() {
'use cache'
cacheLife('produktDaten')
const produkte = await db.query('SELECT * FROM produkte')
return produkte
}
On-Demand-Revalidierung mit cacheTag, updateTag und revalidateTag
Zeitbasiertes Caching ist nützlich — keine Frage. Aber oft will man den Cache gezielt invalidieren, etwa nach einer Datenbankänderung. Dafür bietet Next.js 16 drei Werkzeuge: cacheTag, updateTag und revalidateTag.
Cache-Einträge taggen mit cacheTag
Mit cacheTag vergibt man semantische Tags an gecachte Funktionen oder Komponenten. Das ist im Grunde wie Bookmarks für den Cache:
import { cacheTag, cacheLife } from 'next/cache'
export async function holeBestellungen(userId: string) {
'use cache'
cacheTag('bestellungen', `benutzer-${userId}`)
cacheLife('hours')
return await db.query(
'SELECT * FROM bestellungen WHERE benutzer_id = ?',
[userId]
)
}
updateTag: Sofortige Invalidierung in Server Actions
updateTag ist speziell für Server Actions konzipiert und invalidiert den Cache sofort. Ideal für Read-your-own-writes-Szenarien, bei denen der Benutzer die Änderung unmittelbar sehen muss:
'use server'
import { updateTag } from 'next/cache'
import { redirect } from 'next/navigation'
export async function bestellungAufgeben(formData: FormData) {
const bestellung = await db.bestellungen.create({
data: {
produkt: formData.get('produkt'),
menge: Number(formData.get('menge')),
},
})
// Cache sofort invalidieren
updateTag('bestellungen')
updateTag(`benutzer-${bestellung.benutzerId}`)
redirect(`/bestellungen/${bestellung.id}`)
}
revalidateTag: Stale-While-Revalidate-Semantik
revalidateTag eignet sich für Szenarien, bei denen kurzfristig veraltete Daten akzeptabel sind. Der veraltete Inhalt wird weiterhin ausgeliefert, während im Hintergrund frischer Inhalt generiert wird:
import { revalidateTag } from 'next/cache'
export async function artikelAktualisieren(id: string) {
'use server'
await db.artikel.update({ where: { id }, data: { /* ... */ } })
// Stale-While-Revalidate: Alter Inhalt bleibt sichtbar,
// während neuer generiert wird
revalidateTag('artikel', 'max')
}
Der zentrale Unterschied zwischen den beiden auf einen Blick:
- updateTag: Nur in Server Actions, sofortige Cache-Invalidierung, für unmittelbares Feedback
- revalidateTag: In Server Actions und Route Handlers nutzbar, unterstützt Stale-While-Revalidate
Laufzeitdaten und use cache kombinieren
Hier kommt eine wichtige Einschränkung, die man kennen muss: use cache und Laufzeit-APIs wie cookies() oder headers() können nicht im selben Scope verwendet werden. Das ergibt auch Sinn, wenn man drüber nachdenkt — Laufzeitdaten sind per Definition request-spezifisch und damit nicht cachebar.
Die Lösung? Laufzeitdaten außerhalb der gecachten Funktion lesen und als Argumente weitergeben:
import { cookies } from 'next/headers'
import { Suspense } from 'react'
export default function ProfilSeite() {
return (
<Suspense fallback={<div>Lade Profil...</div>}>
<ProfilInhalt />
</Suspense>
)
}
// Nicht gecachte Komponente liest Laufzeitdaten
async function ProfilInhalt() {
const session = (await cookies()).get('session')?.value
// Sessiondaten als Argument weitergeben
return <GecachtesProfilDetail sessionId={session} />
}
// Gecachte Komponente erhält Daten als Props
async function GecachtesProfilDetail({ sessionId }: { sessionId: string }) {
'use cache'
// sessionId wird Teil des Cache-Schlüssels
const daten = await fetch(`/api/profil/${sessionId}`)
return <div>{/* Profildaten anzeigen */}</div>
}
Dieses Muster ist wirklich elegant: Für jeden unterschiedlichen sessionId-Wert wird ein eigener Cache-Eintrag erstellt. So lassen sich personalisierte Inhalte effizient cachen, ohne auf Laufzeitdaten im gecachten Scope zugreifen zu müssen.
Praxisbeispiel: Alles zusammen auf einer Seite
Genug Theorie — hier ein vollständiges Beispiel, das statischen, gecachten und dynamischen Inhalt auf einer einzelnen Seite kombiniert:
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife, cacheTag } from 'next/cache'
import Link from 'next/link'
export default function ShopSeite() {
return (
<>
{/* Statischer Inhalt — automatisch vorgerendert */}
<header>
<h1>Unser Online-Shop</h1>
<nav>
<Link href="/">Startseite</Link>
{' | '}
<Link href="/kategorien">Kategorien</Link>
</nav>
</header>
{/* Gecachter dynamischer Inhalt — in der statischen Hülle */}
<TopProdukte />
{/* Laufzeit-dynamischer Inhalt — Streaming */}
<Suspense fallback={<p>Lade Warenkorb...</p>}>
<WarenkorbVorschau />
</Suspense>
</>
)
}
// Alle Benutzer sehen dieselben Top-Produkte (stündlich revalidiert)
async function TopProdukte() {
'use cache'
cacheLife('hours')
cacheTag('produkte')
const res = await fetch('https://api.example.com/produkte/top')
const produkte = await res.json()
return (
<section>
<h2>Beliebte Produkte</h2>
<ul>
{produkte.slice(0, 6).map((p: any) => (
<li key={p.id}>
<h3>{p.name}</h3>
<p>{p.preis} €</p>
</li>
))}
</ul>
</section>
)
}
// Personalisiert pro Benutzer basierend auf Cookies
async function WarenkorbVorschau() {
const warenkorb = (await cookies()).get('warenkorb')?.value
const anzahl = warenkorb ? JSON.parse(warenkorb).length : 0
return (
<aside>
<p>Artikel im Warenkorb: {anzahl}</p>
</aside>
)
}
Beim Prerendering werden der Header (statisch) und die Top-Produkte (gecacht) Teil der statischen Hülle, zusammen mit dem Fallback-UI für den Warenkorb. Der Benutzer sieht sofort den vorgerenderten Inhalt, und nur der personalisierte Warenkorb wird per Streaming nachgeliefert. Das ist eine spürbar bessere User Experience.
Interleaving: Gecachte und dynamische Komponenten verschachteln
Ein häufiges Muster in der Praxis ist das Verschachteln von gecachten und nicht gecachten Komponenten. Mit dem Pass-Through-Prinzip können children oder andere Slots durch gecachte Komponenten geleitet werden, ohne den Cache-Eintrag zu beeinflussen:
export default async function Seite() {
const liveDaten = await holeLiveDaten()
return (
<GecachterWrapper header={<h1>Dashboard</h1>}>
<DynamischeKomponente daten={liveDaten} />
</GecachterWrapper>
)
}
async function GecachterWrapper({
header,
children,
}: {
header: React.ReactNode
children: React.ReactNode
}) {
'use cache'
const navigation = await fetch('/api/navigation')
const navDaten = await navigation.json()
return (
<div>
{header}
<nav>
{navDaten.map((item: any) => (
<a key={item.id} href={item.url}>{item.titel}</a>
))}
</nav>
{children}
</div>
)
}
In diesem Beispiel wird die Navigation gecacht, während header und children einfach durchgereicht werden. DynamischeKomponente bleibt also vollständig dynamisch, obwohl sie innerhalb einer gecachten Komponente gerendert wird. Ziemlich clever gelöst, oder?
use cache: remote und use cache: private
Neben dem Standard-use cache gibt es zwei spezialisierte Varianten, die man kennen sollte.
use cache: remote
Standardmäßig werden Cache-Einträge im In-Memory-LRU-Speicher des Servers gehalten. In Serverless-Umgebungen bedeutet das allerdings, dass Cache-Einträge zwischen Requests verloren gehen können — da jeder Request potenziell auf einer anderen Instanz läuft.
use cache: remote ermöglicht es der Hosting-Plattform, einen dedizierten Cache-Handler bereitzustellen (wie Redis oder eine KV-Datenbank). Das reduziert Anfragen an die Datenquelle, bringt aber zusätzliche Netzwerk-Latenz und potenziell Plattformkosten mit sich. Ein klassischer Tradeoff.
use cache: private
Für Compliance-Anforderungen oder wenn man den Code nicht umstrukturieren kann, um Laufzeitdaten als Argumente zu übergeben, gibt es use cache: private. Diese Variante erlaubt den Zugriff auf Laufzeit-APIs innerhalb des gecachten Scopes — allerdings mit Einschränkungen hinsichtlich der Cache-Wiederverwendbarkeit. Nutzt man das zu großzügig, verliert man den eigentlichen Caching-Vorteil.
Migration von bestehenden Konfigurationen
Wer eine bestehende Next.js-Anwendung auf Cache Components umstellt, muss einige Route-Segment-Konfigurationen anpassen. Die gute Nachricht: Es ist überschaubarer, als man denken könnte. Hier die wichtigsten Migrationsszenarien.
dynamic = "force-dynamic" entfernen
Mit Cache Components sind alle Seiten standardmäßig dynamisch. Die Konfiguration force-dynamic ist schlicht nicht mehr nötig:
// Vorher (Next.js 15)
export const dynamic = 'force-dynamic'
export default function Seite() {
return <div>...</div>
}
// Nachher (Next.js 16 mit Cache Components)
// Einfach entfernen — Seiten sind standardmäßig dynamisch
export default function Seite() {
return <div>...</div>
}
Weniger Code ist hier tatsächlich besser.
dynamic = "force-static" ersetzen
Statt force-static verwendet man jetzt use cache mit einem langen Cache-Profil:
// Vorher
export const dynamic = 'force-static'
export default async function Seite() {
const daten = await fetch('https://api.example.com/daten')
return <div>...</div>
}
// Nachher
import { cacheLife } from 'next/cache'
export default async function Seite() {
'use cache'
cacheLife('max')
const daten = await fetch('https://api.example.com/daten')
return <div>...</div>
}
revalidate durch cacheLife ersetzen
// Vorher
export const revalidate = 3600
export default async function Seite() {
return <div>...</div>
}
// Nachher
import { cacheLife } from 'next/cache'
export default async function Seite() {
'use cache'
cacheLife('hours')
return <div>...</div>
}
fetchCache ist nicht mehr nötig
Mit use cache wird alles innerhalb des gecachten Scopes automatisch gecacht. Die separate fetchCache-Konfiguration entfällt komplett.
runtime = "edge" ist nicht mehr unterstützt
Cache Components erfordern die Node.js Runtime. Die Edge Runtime wird nicht unterstützt und erzeugt Fehler. Wer bisher auf der Edge Runtime gecacht hat, muss auf Node.js umstellen. Das ist vielleicht der einzige Punkt, der bei manchen Projekten etwas mehr Aufwand bedeuten könnte.
Navigation und React Activity
Ein oft übersehenes (aber ziemlich cooles) Feature von Cache Components: Wenn cacheComponents aktiviert ist, verwendet Next.js Reacts neue <Activity>-Komponente aus React 19.2 für die clientseitige Navigation.
Anstatt die vorherige Route beim Navigieren zu unmounten, setzt Next.js den Activity-Modus auf hidden. Das bringt einige handfeste Vorteile:
- Zustandserhaltung: Formulareingaben, Scroll-Positionen und aufgeklappte Abschnitte bleiben beim Zurücknavigieren erhalten
- Schnellere Back-Navigation: Die vorherige Route erscheint sofort mit ihrem gespeicherten Zustand
- Saubere Effekte: Effects werden beim Verstecken aufgeräumt und beim Wiederanzeigen neu erstellt
Next.js verwendet Heuristiken, um einige kürzlich besuchte Routen im hidden-Zustand zu halten, während ältere Routen aus dem DOM entfernt werden. Man merkt den Unterschied sofort — besonders bei formularlastigen Anwendungen.
Debugging und Fehlerbehebung
Beim Arbeiten mit Cache Components können einige typische Probleme auftreten. Hier die wichtigsten Debugging-Strategien, die mir in der Praxis geholfen haben.
Verbose Logging aktivieren
Für detaillierte Cache-Logs setzt man die Umgebungsvariable NEXT_PRIVATE_DEBUG_CACHE:
# Entwicklung
NEXT_PRIVATE_DEBUG_CACHE=1 npm run dev
# Produktion
NEXT_PRIVATE_DEBUG_CACHE=1 npm run start
In der Entwicklung erscheinen Console-Logs aus gecachten Funktionen mit einem Cache-Präfix. Sehr hilfreich, um nachzuvollziehen, ob eine Komponente tatsächlich aus dem Cache kommt oder frisch gerendert wird.
Build-Hangs vermeiden
Ein häufiges Problem, das einen anfangs verrückt machen kann: Der Build hängt sich auf, wenn Promises, die auf Laufzeitdaten zeigen, innerhalb eines use cache-Scopes aufgelöst werden sollen. Nach 50 Sekunden Timeout erscheint eine Fehlermeldung.
Das passiert typischerweise in diesen Szenarien:
- Laufzeit-Daten-Promises als Props an gecachte Komponenten übergeben
- Auf Laufzeitdaten über Closures zugreifen
- Daten aus geteiltem Speicher (Maps) innerhalb von
use cachelesen
// FALSCH: Build hängt sich auf
async function Dynamisch() {
const cookieStore = cookies()
return <Gecacht promise={cookieStore} />
}
async function Gecacht({ promise }: { promise: Promise<unknown> }) {
'use cache'
const daten = await promise // Wartet auf Laufzeitdaten beim Build!
return <p>...</p>
}
// RICHTIG: Cookie-Wert außerhalb auflösen
async function Dynamisch() {
const cookieStore = await cookies()
const wert = cookieStore.get('meinCookie')?.value ?? ''
return <Gecacht cookieWert={wert} />
}
async function Gecacht({ cookieWert }: { cookieWert: string }) {
'use cache'
// cookieWert ist ein einfacher String — kein Problem
const daten = await fetch(`/api/daten?cookie=${cookieWert}`)
return <p>...</p>
}
React.cache-Isolation beachten
React.cache arbeitet innerhalb von use cache-Grenzen in einem isolierten Scope. Werte, die über React.cache außerhalb eines gecachten Bereichs gespeichert werden, sind darin nicht sichtbar. Das kann anfangs verwirrend sein. Die Lösung: Daten über Funktionsargumente in den gecachten Scope übergeben statt über React.cache.
Wann sollte man was cachen?
Zum Schluss noch einige Faustregeln für die richtige Caching-Strategie. Diese Tabelle habe ich mir als Referenz zusammengestellt — vielleicht hilft sie euch auch weiter:
| Szenario | Empfohlener Ansatz |
|---|---|
| Statische Inhalte (About-Seite, Impressum) | Automatisches Prerendering — kein use cache nötig |
| CMS-Inhalte (Blog, Produkte) | use cache + cacheTag + revalidateTag |
| API-Daten, die sich stündlich ändern | use cache + cacheLife('hours') |
| Benutzerspezifische Daten | Laufzeitdaten extrahieren + als Argument an use cache übergeben |
| Echtzeit-Feeds, personalisierte Inhalte | <Suspense>-Boundary ohne Cache |
| Teure Berechnungen | use cache auf Funktionsebene |
| Datenbank-Mutationen mit sofortigem Feedback | updateTag in Server Action |
| Hintergrund-Aktualisierung mit Toleranz für Verzögerung | revalidateTag mit max-Profil |
Fazit: Explizit statt implizit — und endlich vorhersagbar
Cache Components und die use cache-Direktive markieren einen echten Paradigmenwechsel im Next.js-Caching. Statt sich auf implizite, oft schwer nachvollziehbare Caching-Regeln zu verlassen, hat man jetzt die volle Kontrolle darüber, was gecacht wird, wie lange, und wann der Cache invalidiert wird.
Die wichtigsten Erkenntnisse auf einen Blick:
- Opt-in statt Opt-out: Kein implizites Caching mehr — man entscheidet aktiv, was gecacht wird
- Granulare Kontrolle: Caching auf Datei-, Komponenten- oder Funktionsebene
- Partial Prerendering: Statische, gecachte und dynamische Inhalte koexistieren auf einer Seite
- Klare Revalidierungs-APIs:
updateTagfür sofortige,revalidateTagfür verzögerte Invalidierung - Vorhersagbares Verhalten: Cache-Schlüssel werden automatisch aus Argumenten und Closures generiert
Die Migration von bestehenden Route-Segment-Konfigurationen ist überschaubar, und die neuen APIs sind intuitiver als ihre Vorgänger. Wer bisher mit dem Caching im App Router gekämpft hat, findet in Cache Components endlich ein System, das transparent und nachvollziehbar arbeitet.
Meiner Meinung nach ist es einer der besten Schritte, die das Next.js-Team in letzter Zeit gemacht hat. Es ist an der Zeit, das implizite Caching hinter sich zu lassen.