Next.js 16 cacheComponents: Migration von 91.000 Seiten ohne Ausfallzeit
Dein Deploy startet um 2:14 Uhr morgens. Einundneunzigtausend Produktseiten – Kategoriebäume, lokalisierte Varianten über 14 Märkte hinweg, redaktionelle SEO-Inhalte – wechseln von Next.js 14s App Router zu v16s cacheComponents API. Du beobachtest die erste Request-Kette in Vercels Logs. TTFB springt auf 1,8 Sekunden. Dein Slack piept. Achtzehn Monate hast du gegen das alte Default-Cache-Modell gekämpft: veraltete Preise, revalidateTag-Aufrufe, die zu spät feuerten, Support-Tickets, die "die Website" beschuldigten. Next.js 15 schaltete Caching standardmäßig aus; v16 gab dir cacheComponents, um gezielt wieder einzuschalten. Du hast dich für die Migration entschieden, statt langsam durch tausend Cache-Bugs zu sterben. Jetzt starrst du auf echte Traffic, echte Fehler und ein Production-Incident zwei Stunden vor Sonnenaufgang. Hier ist, was zusammenbrach, was hielt, und die Performance-Unterschiede, die keine Benchmark-Suite vorhergesagt hätte.
Inhaltsverzeichnis
- Das Caching-Problem, das wir wirklich hatten
- Was sich in Next.js 15 und 16 änderte
- cacheComponents verstehen
- Unsere Migrationsstrategie für 91.000 Seiten
- Implementierung: Schritt für Schritt
- Performance-Ergebnisse und Benchmarks
- Fallstricke und Tücken
- Wann du cacheComponents verwenden solltest und wann nicht
- Häufig gestellte Fragen

Das Caching-Problem, das wir wirklich hatten
Lass mich das Szenario zeichnen. In Next.js 14s App Router wurden fetch-Anfragen in Server Components standardmäßig gecacht. Der Data Cache persistierte über Deployments hinweg. Der Full Route Cache speicherte gerenderte HTML und RSC-Payloads zur Build-Zeit. Und der Router Cache auf der Client-Seite behielt vorgeladene Segmente für... nun ja, länger als erwartet.
Für eine Site mit 91.000 Seiten erzeugte dieser Default-Cache-Everything-Ansatz zwei Probleme:
Veraltete Daten überall. Produktpreise wurden in unserem Headless CMS (Sanity, in unserem Fall) aktualisiert, aber die gecachten Fetch-Ergebnisse blieben. Wir hatten revalidateTag-Aufrufe über 47 verschiedene Server Actions verteilt. Einen Tag verpassen? Ein Kunde sieht den gestrigen Preis. Wir hatten buchstäblich einen Slack-Kanal namens #cache-crimes, wo das Content-Team veraltete Seiten meldete.
Build-Zeiten aus der Hölle. Die vollständige statische Generierung von 91.000 Seiten dauerte über 3 Stunden. Wir waren zu ISR mit revalidate: 3600 für die meisten Seiten gewechselt, aber die Interaktion zwischen ISR, dem Data Cache und On-Demand-Revalidation war wirklich schwer zu durchschauen. Neue Entwickler im Team brauchten ihre ersten zwei Wochen nur zum Verständnis der Caching-Schichten.
Die Mental-Model-Last
Hier ist, was ich denke, dass Menschen unterschätzen: die kognitiven Kosten implizites Caching. Wenn Caching der Standard ist und du aussteigst, erfordert jede neue Component, dass du fragst "sollte das gecacht werden?" und dann vergisst, die richtige Direktive hinzuzufügen, wenn die Antwort nein ist. Wenn No-Caching der Standard ist und du einsteigst, denkst du nur über Caching nach, wenn du es aktiv willst. Das ist ein fundamental anderes – und besseres – Mental Model.
Was sich in Next.js 15 und 16 änderte
Next.js 15 war die große philosophische Verschiebung. Das Team flipte die Defaults:
| Verhalten | Next.js 14 | Next.js 15 | Next.js 16 |
|---|---|---|---|
fetch() in Server Components |
Standardmäßig gecacht | Standardmäßig nicht gecacht | Standardmäßig nicht gecacht |
| Route Handlers (GET) | Standardmäßig gecacht | Standardmäßig nicht gecacht | Standardmäßig nicht gecacht |
| Client Router Cache | 30s (dynamisch) / 5min (statisch) | 0s für Page Segments | 0s Standard, konfigurierbar |
| Full Route Cache | Aktiviert für statische Routes | Gleich | Gleich, mit cacheLife Verfeinerungen |
| Component-Level Caching | unstable_cache |
use cache Direktive (experimentell) |
cacheComponents API (stabil) |
Next.js 15 führte die use cache Direktive als experimentelles Feature hinter einem Flag ein. Next.js 16, Anfang 2025 veröffentlicht, stabilisierte dies als die cacheComponents Konfigurationsoption und die zugehörige "use cache" Direktive, zusammen mit cacheLife zum Definieren benutzerdefinierter Cache-Profile und cacheTag für gezielte Invalidation.
Die Schlüsseleinsicht: Caching verschob sich von implizitem Framework-Verhalten zu expliziter Entwicklerentscheidung auf Component-Level. Das ist ein großes Ding für große Sites.
cacheComponents verstehen
Die cacheComponents Funktion in next.config.js aktiviert Component-Level Caching durch die "use cache" Direktive. Hier ist die grundlegende Einrichtung:
// next.config.js (Next.js 16)
const nextConfig = {
experimental: {
cacheComponents: true,
},
};
module.exports = nextConfig;
Einmal aktiviert, kannst du "use cache" oben in jeder async Server Component, Server Action oder sogar Layoutdatei hinzufügen:
// app/products/[slug]/page.tsx
"use cache";
import { cacheLife, cacheTag } from 'next/cache';
export default async function ProductPage({ params }: { params: { slug: string } }) {
cacheLife('products'); // benutzerdefiniertes Cache-Profil
cacheTag(`product-${params.slug}`);
const product = await fetchProduct(params.slug);
return (
<div>
<h1>{product.name}</h1>
<ProductDetails product={product} />
<DynamicPricing productId={product.id} /> {/* Diese Component wird NICHT gecacht */}
</div>
);
}
cacheLife Profile
Hier wird es interessant für große Sites. Du definierst benannte Cache-Profile in next.config.js:
const nextConfig = {
experimental: {
cacheComponents: true,
cacheLife: {
products: {
stale: 300, // serve stale für 5 Minuten
revalidate: 3600, // revalidate nach 1 Stunde
expire: 86400, // hart expirieren nach 24 Stunden
},
editorial: {
stale: 3600,
revalidate: 86400,
expire: 604800, // 7 Tage
},
navigation: {
stale: 86400,
revalidate: 604800,
expire: 2592000, // 30 Tage
},
},
},
};
Das Drei-Schicht-Modell (stale, revalidate, expire) passt schön zur Stale-While-Revalidate-Semantik. Während des stale Fensters wird gecachter Content sofort bereitgestellt. Nach stale aber vor expire startet eine Hintergrund-Revalidation. Nach expire ist der Cache-Eintrag weg.
cacheTag für Invalidation
Die cacheTag Funktion ersetzt das alte revalidateTag Muster durch etwas Zusammensetzbares:
import { revalidateTag } from 'next/cache';
// In einem Webhook-Handler oder Server Action:
export async function handleProductUpdate(productSlug: string) {
revalidateTag(`product-${productSlug}`);
revalidateTag('product-listing'); // invalidate auch Listing-Seiten
}
Dieser Teil änderte sich nicht viel von Next.js 15, funktioniert aber viel besser mit cacheComponents, weil du spezifische gecachte Components taggst, nicht opaque Framework-Level Caches.

Unsere Migrationsstrategie für 91.000 Seiten
Wir haben das nicht in einem Schuss gemacht. Mit 91.000 Seiten über 14 Locales wäre eine Big-Bang-Migration unverantwortlich gewesen. So haben wir es aufgeteilt:
Phase 1: Upgrade zu Next.js 16, keine Cache-Änderungen (Woche 1-2)
Wir upgradeteten von Next.js 14.2 zu 16.0, ohne cacheComponents zu aktivieren. Das allein änderte das Verhalten, weil fetch-Anfragen nicht länger standardmäßig gecacht wurden. Wir erwarteten TTFB-Regressionen und bekamen sie:
- Durchschnittlicher TTFB stieg von 180ms auf 340ms auf Produktseiten
- Origin-Server-Last stieg um ~60% (unser Sanity CDN hielt sich, aber unsere benutzerdefinierten API-Endpoints nicht)
- ISR-Revalidation wurde tatsächlich schneller, weil es weniger Cache-Status zu verwalten gab
Das bestätigte, was wir verdachteten: Wir waren stark auf implizites Caching angewiesen, und viele unserer Seiten brauchten Caching – nur explizites, absichtliches Caching.
Phase 2: Audit und Klassifizierung von Seiten (Woche 3)
Wir kategorisiertenRoute in unserem App:
| Seitentyp | Anzahl | Cache-Strategie | cacheLife Profil |
|---|---|---|---|
| Produktdetail-Seiten | 42.000 | Cache mit Product-Tag | products (5min stale / 1h revalidate) |
| Kategorielisting-Seiten | 3.200 | Cache mit Category-Tag | products (5min stale / 1h revalidate) |
| Redaktionelle/Blog-Seiten | 8.400 | Aggressiv cachen | editorial (1h stale / 24h revalidate) |
| Lokalisierte Varianten | 31.647 | Gleich wie Basis-Seite | Geerbt von Basis |
| Konto/dynamische Seiten | 6.000 | Kein Cache | N/A |
Phase 3: Aktiviere cacheComponents, füge Direktiven hinzu (Woche 4-6)
Wir aktivierten das Flag und begannen, "use cache" Direktiven hinzuzufügen. Die Schlüsseldecision: Wir cachteten auf Page-Level für die meisten Routes, aber auf Component-Level für Seiten mit gemischtem statischem/dynamischem Content.
Für Produktseiten wurde die Produktinfo und Bilder gecacht, aber die Pricing-Component und Bestandsstatus wurden nicht gecacht:
// components/ProductInfo.tsx
"use cache";
import { cacheLife, cacheTag } from 'next/cache';
export async function ProductInfo({ slug }: { slug: string }) {
cacheLife('products');
cacheTag(`product-${slug}`, 'product-info');
const product = await getProduct(slug);
return (
<section>
<h1>{product.name}</h1>
<p>{product.description}</p>
<ProductImages images={product.images} />
</section>
);
}
// components/DynamicPricing.tsx
// KEINE "use cache" Direktive – immer aktuell
export async function DynamicPricing({ productId }: { productId: string }) {
const pricing = await getPricing(productId); // trifft jedes Mal die Pricing-API
return (
<div className="pricing">
<span className="price">${pricing.current}</span>
{pricing.onSale && <span className="was-price">${pricing.original}</span>}
</div>
);
}
Phase 4: Webhook-Integration (Woche 7)
Wir leiteten unsere Sanity Webhooks um, um revalidateTag mit den richtigen Tags aufzurufen. Das war eigentlich einfacher als unser altes Setup, weil Tags jetzt im Code explizit waren, nicht über Fetch-Optionen verteilt.
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const body = await request.json();
const secret = request.headers.get('x-webhook-secret');
if (secret !== process.env.REVALIDATION_SECRET) {
return new Response('Unauthorized', { status: 401 });
}
switch (body._type) {
case 'product':
revalidateTag(`product-${body.slug.current}`);
revalidateTag('product-listing');
break;
case 'category':
revalidateTag(`category-${body.slug.current}`);
revalidateTag('navigation');
break;
case 'article':
revalidateTag(`article-${body.slug.current}`);
break;
}
return new Response('OK', { status: 200 });
}
Implementierung: Schritt für Schritt
Wenn du eine ähnliche Migration durchführst, hier ist das praktische Playbook, das wir empfehlen würden (und das wir jetzt für Next.js Entwicklungsprojekte bei Social Animal verwenden):
Schritt 1: Aktiviere das Flag
// next.config.js
module.exports = {
experimental: {
cacheComponents: true,
cacheLife: {
// Starte mit sensiblen Standards
default: {
stale: 60,
revalidate: 900,
expire: 86400,
},
},
},
};
Schritt 2: Finde deine Hot Paths
Verwende deine Analytik, um Seiten zu identifizieren, die den meisten Traffic bekommen und wo TTFB wichtig ist. Für uns waren es Kategorieseiten (hoher Traffic, relativ stabiler Content) und Produktseiten (hoher Traffic, moderat dynamischer Content).
Schritt 3: Füge `"use cache"` Top-Down hinzu
Fang mit Layouts an. Wenn dein Root-Layout Navigations-Daten abruft, cache das zuerst – es ist die höchste Auswirkung, das niedrigste Risiko:
// app/layout.tsx
// Anmerkung: "use cache" auf Layouts cacht die Layout-Shell
// Child Pages rendern immer noch unabhängig
import { Navigation } from '@/components/Navigation';
export default async function RootLayout({ children }) {
return (
<html>
<body>
<Navigation /> {/* Diese Component hat ihre eigene "use cache" */}
{children}
</body>
</html>
);
}
Schritt 4: Richte Monitoring ein
Wir verwendeten Vercels eingebaute Analytik plus benutzerdefiniertes Logging, um Cache-Hit-Raten zu verfolgen. In der ersten Woche nach Aktivierung von cacheComponents war unsere Cache-Hit-Rate nur 34%. Nach Tuning der stale Dauer stieg sie auf 78%.
Performance-Ergebnisse und Benchmarks
Hier sind die echten Zahlen nach der vollständigen Migration, gemessen über einen 30-Tage-Zeitraum auf Vercels Pro-Plan:
| Metrik | Vorher (Next.js 14) | Nach Phase 1 (v16, kein Cache) | Nach vollständiger Migration |
|---|---|---|---|
| Durchschn. TTFB (Produktseiten) | 180ms | 340ms | 95ms |
| Durchschn. TTFB (Kategorieseiten) | 220ms | 410ms | 72ms |
| Durchschn. TTFB (redaktionelle Seiten) | 150ms | 280ms | 45ms |
| P99 TTFB (alle Seiten) | 1.200ms | 2.100ms | 380ms |
| Build-Zeit (vollständig) | 3h 12min | 2h 48min | 48min |
| Vercel-Funktionsaufrufe/Tag | 2,4M | 3,8M | 1,1M |
| Monatliche Vercel-Rechnung | ~$840 | ~$1.200 | ~$520 |
| Cache-Hit-Rate | Unbekannt (implizit) | N/A | 78% |
| Veraltete Content-Incidents (#cache-crimes) | 8-12/Woche | 0 | 1-2/Monat |
Die Build-Zeit-Verbesserung verdient eine Erklärung. Mit cacheComponents bewegten wir uns weg von der Generierung aller 91.000 Seiten zur Build-Zeit. Stattdessen generierten wir statisch nur die Top-5.000 Seiten (nach Traffic) und ließen den Rest On-Demand mit Caching generieren. Die cacheComponents Direktive bedeutete, dass diese On-Demand Seiten nach dem ersten Besuch gecacht wurden, mit cacheLife zur Kontrolle der Aktualität.
Die Vercel-Rechnungs-Reduzierung war signifikant. Weniger Funktionsaufrufe (wegen explizitem Component-Caching) plus kürzere Build-Zeiten bedeuteten echte Kostenersparnisse. Diese ~$320/Monat Reduktion bezahlt sich selbst.
Fallstricke und Tücken
Serialisierungs-Grenzen
Die "use cache" Direktive erzeugt eine Serialisierungs-Grenze. Alles, das als Props in eine gecachte Component übergeben wird, muss serialisierbar sein. Wir hatten mehrere Components, die Callback-Funktionen oder React-Elemente als Props empfingen – die brachen sofort. Die Lösung war das Umstrukturieren zu Composition-Patterns statt:
// ❌ Das bricht mit "use cache"
"use cache";
export async function ProductCard({ product, onAddToCart }) {
// onAddToCart ist eine Funktion – nicht serialisierbar!
}
// ✅ Das funktioniert
"use cache";
export async function ProductCard({ product }) {
return (
<div>
<h2>{product.name}</h2>
{/* AddToCart ist eine Client Component, nicht gecacht */}
<AddToCartButton productId={product.id} />
</div>
);
}
Dynamische Params und Cache-Key-Explosion
Mit 91.000 Seiten, jede mit einzigartigen Params, ist der Cache-Key-Raum riesig. Wir trafen Vercels Edge-Cache-Grenzen in der ersten Woche und mussten strategischer darüber sein, welche Seiten lange expire Werte bekamen. Low-Traffic Long-Tail-Seiten bekamen kürzere Cache-Dauer.
Die `Date.now()` Falle
Jede Component mit "use cache", die Date.now() oder new Date() im gecachten Funktion aufruft, wird diesen Timestamp cachen. Wir fanden das in einer "zuletzt aktualisiert" Anzeige, die stundenlang die gleiche Zeit zeigte. Die Lösung: Verschiebe zeitempfindliche Logik zu einer Client Component oder einer ungecachten Server Component.
Verschachtelte Cache-Grenzen
Wenn du gecachte Components in anderen gecachten Components verschachtelst, hat der innere Cache seinen eigenen Lebenszyklus. Das ist mächtig, aber verwirrend. Wir etablierten eine Team-Konvention: Cache auf Page-Level ODER Component-Level, nicht beides, es sei denn, es gibt einen klaren Grund.
Wann du cacheComponents verwenden solltest und wann nicht
Verwende es wenn:
- Du mehr als einige hundert Seiten hast und ISR Build-Zeiten sind schmerzhaft
- Dein Content hat klare Frische-Anforderungen, die nach Abschnitt variieren
- Du feingranulare Kontrolle brauchst über das, was gecacht wird vs. immer-aktuell
- Du auf Vercel oder einer Plattform laufen, die Next.js Cache-Layer unterstützt
- Du Infrastruktur-Kosten auf hochfrequenten Sites reduzieren möchtest
Verwende es nicht wenn:
- Deine Site klein genug ist, dass vollständiges SSG funktioniert
- Jede Seite ist vollständig dynamisch (überall benutzerspezifischer Content)
- Du bist nicht auf einer Hosting-Plattform, die Next.js Caching-Infrastruktur unterstützt
- Dein Team ist neu in Next.js – werde zuerst mit den Basics vertraut
Wenn du bewertest, ob dein Projekt dieses Level von Caching-Kontrolle braucht, oder ob ein anderes Framework wie Astro eine bessere Passung für deine Content-Heavy-Site sein könnte, das lohnt sich, vorher zu durchdenken, bevor du dich auf eine Migration commitest.
Für Projekte, wo Content von mehreren Headless-CMS-Quellen kommt, funktioniert das cacheTag System in Next.js 16 wunderbar mit Headless-CMS-Architekturen – jeder Content-Typ bekommt seinen eigenen Invalidations-Kanal.
Häufig gestellte Fragen
Was ist cacheComponents in Next.js 16?
cacheComponents ist eine experimentelle Konfigurationsoption in Next.js 16, die die "use cache" Direktive für Server Components aktiviert. Es lässt dich explizit markieren, welche Components gecacht werden sollen, und benutzerdefinierte Cache-Profile mit cacheLife definieren. Es ist die stabile Evolution der use cache Direktive, die in Next.js 15 experimentell war.
Wie unterscheidet sich cacheComponents von ISR (Incremental Static Regeneration)?
ISR cacht ganze Seiten und revalidiert sie nach einem Zeitplan. cacheComponents lässt dich einzelne Components innerhalb einer Seite cachen, jede mit verschiedenen Cache-Lifetimes. Eine einzelne Seite kann einen Header cachen, der 24 Stunden gecacht ist, Produktinfo 1 Stunde gecacht, und Pricing, das nie gecacht ist. ISR kann das nicht – es ist alles oder nichts auf Page-Level.
Muss ich auf Vercel sein, um cacheComponents zu verwenden?
Nein, aber die Erfahrung ist beste auf Vercel, weil die Caching-Infrastruktur eng integriert ist. Self-gehostete Next.js Deployments können cacheComponents mit dem File-System-Cache-Adapter verwenden, aber du bekommst nicht die Edge-Verteilungsvorteile. Plattformen wie Netlify und Cloudflare fügen Unterstützung hinzu, aber Stand Mitte 2026 bleibt Vercel die vollständigste Implementierung.
Wie invalidiere ich gecachte Components in Next.js 16?
Du verwendest cacheTag() innerhalb deiner gecachten Component, um Tags zuzuweisen, dann aufruf revalidateTag('tag-name') von einer Server Action, Route Handler oder Webhook Endpoint. Das invalidiert alle gecachten Components mit diesem Tag. Es ist die gleiche API von Next.js 15, aber es ist jetzt nützlicher, weil du spezifische gecachte Components taggst, nicht opaque Framework-Caches.
Wird cacheComponents meine Vercel-Rechnung reduzieren? Es kann die Kosten signifikant reduzieren. In unserem Fall fielen Funktionsaufrufe um 54%, weil gecachte Component-Responses von der Cache-Layer bereitgestellt wurden, statt Serverless-Funktionen aufzurufen. Die Build-Zeit-Reduktion spart auch auf Build-Minuten. Deine Kilometerleistung variiert basierend auf Traffic-Mustern und Cache-Hit-Raten – überprüf Vercels Preis-Rechner mit deinem aktuellen Verbrauch.
Was passiert, wenn ich "use cache" zu einer Component hinzufüge, die nicht-serialisierbare Props empfängt?
Du wirst einen Build-Fehler bekommen. Die "use cache" Direktive erzeugt eine Serialisierungs-Grenze, also müssen alle Props serialisierbar sein (Strings, Nummern, plain Objects, Arrays). Funktionen, React-Elemente, Class-Instanzen und andere nicht-serialisierbare Werte werden den Build kaputt machen. Strukturiere deine Component um, um nur Daten-Props zu akzeptieren und handle Interaktivität in Child Client Components.
Kann ich cacheComponents mit React Server Components aus anderen Frameworks verwenden?
Nein. cacheComponents ist eine Next.js-spezifische Funktion, die auf top von React's Server Components baut. Während die "use cache" Direktive-Syntax eventuell ein React-Standard wird, die cacheLife Profile und cacheTag System sind Next.js APIs. Wenn du ein Framework wie Remix oder ein benutzerdefiniertes RSC-Setup nutzt, brauchst du verschiedene Caching-Strategien.
Wie lange dauert es, eine große Next.js Site zu cacheComponents zu migrieren? Für unsere 91.000-Seiten-Site mit einem Team von 4 Entwicklern dauerte die vollständige Migration 7 Wochen inklusive Testing und Monitoring. Eine kleinere Site (unter 10.000 Seiten) mit einem einfacheren Daten-Modell könnte es wahrscheinlich in 1-2 Wochen tun. Die eigentlichen Code-Änderungen sind unkompliziert – die Zeit geht in Audit deiner Caching-Bedürfnisse, Testen von Invalidations-Flows und Monitoring von Cache-Hit-Raten nach Deployment. Wenn du das nicht allein tun möchtest, kontaktiere uns – wir haben das jetzt schon ein paar Mal gemacht.