Next.js 16 Cache Components: use cache-Direktive und das neue Caching-Modell erklärt

Next.js 16 führt Cache Components ein: ein opt-in Caching-Modell mit der use cache-Direktive, cacheLife-Profilen und On-Demand-Revalidierung. So funktioniert das neue System in der Praxis.

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:

  1. Build-ID: Eindeutig pro Build — ein neuer Build invalidiert alle Cache-Einträge
  2. Funktions-ID: Ein Hash basierend auf Position und Signatur der Funktion im Code
  3. Serialisierbare Argumente: Props (bei Komponenten) oder Funktionsargumente
  4. 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:

Profilstale (Client)revalidate (Server)expire
seconds30 Sekunden1 Sekunde1 Minute
minutes5 Minuten1 Minute1 Stunde
hours5 Minuten1 Stunde1 Tag
days5 Minuten1 Tag1 Woche
weeks5 Minuten1 Woche1 Monat
max5 Minuten1 MonatUnbegrenzt

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 cache lesen
// 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:

SzenarioEmpfohlener 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 ändernuse cache + cacheLife('hours')
Benutzerspezifische DatenLaufzeitdaten extrahieren + als Argument an use cache übergeben
Echtzeit-Feeds, personalisierte Inhalte<Suspense>-Boundary ohne Cache
Teure Berechnungenuse cache auf Funktionsebene
Datenbank-Mutationen mit sofortigem FeedbackupdateTag in Server Action
Hintergrund-Aktualisierung mit Toleranz für VerzögerungrevalidateTag 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: updateTag für sofortige, revalidateTag fü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.

Über den Autor Editorial Team

Our team of expert writers and editors.