Next.js 16 cacheComponents: Migrierung von 91.000 Seiten vom App Router Caching

Wir betrieben einen großen E-Commerce-Katalog auf Next.js 14's App Router für etwa 18 Monate, als Next.js 16 veröffentlicht wurde. 91.247 Seiten. Produktlisten, Kategoriestrukturen, redaktionelle Inhalte, lokalisierte Varianten über 14 Märkte hinweg. Das alte Caching-Modell – wo Server Components standardmäßig gecacht wurden – war zu einem Minenfeld aus veralteten Datenbuggen und revalidateTag-Spaghetti geworden. Als das Next.js-Team cacheComponents ankündigte und die Verschiebung auf No-Caching-by-Default in Next.js 15 (in v16 weitergeführt und verfeinert), wussten wir, dass es Zeit war. Dies ist die Geschichte dieser Migration: was funktionierte, was nicht, und die Performance-Zahlen auf der anderen Seite.

Inhaltsverzeichnis

Next.js 16 cacheComponents: Migrierung von 91.000 Seiten vom App Router Caching

Das Caching-Problem, das wir wirklich hatten

Lassen Sie mich das Bild zeichnen. Im App Router von Next.js 14 wurden Fetch-Anfragen in Server Components standardmäßig gecacht. Der Data Cache blieb über Deployments hinweg bestehen. Der Full Route Cache speicherte gerendertes HTML und RSC-Payloads zur Build-Zeit. Und der Router Cache auf der Client-Seite hielt vorgefetchte Segmente herum – für... nun, länger als man erwarten würde.

Für eine Website mit 91.000 Seiten erzeugte dieser Standardansatz zum Cachen alles zwei Kategorien von Problemen:

Veraltete Daten überall. Produktpreise wurden in unserem Headless CMS (in unserem Fall Sanity) aktualisiert, aber die gecachten Fetch-Ergebnisse blieben bestehen. Wir hatten revalidateTag-Aufrufe über 47 verschiedene Server Actions verteilt. Eine Tag verpasst? Ein Kunde sieht den Preis von gestern. Wir hatten buchstäblich einen Slack-Kanal namens #cache-crimes, wo das Content-Team veraltete Seiten meldete.

Buildzeiten aus der Hölle. Die vollständig statische Generierung von 91.000 Seiten dauerte über 3 Stunden. Wir waren zu ISR mit revalidate: 3600 für die meisten Seiten übergegangen, aber die Interaktion zwischen ISR, dem Data Cache und On-Demand-Revalidierung war wirklich schwer zu verstehen. Neue Entwickler im Team würden ihre ersten zwei Wochen damit verbringen, einfach die Caching-Layer zu verstehen.

Die Cognitive Load des impliziten Cachings

Hier ist, was ich glaube, dass Menschen unterschätzen: die Cognitive Load des impliziten Cachings. Wenn Caching die Standardeinstellung ist und Sie sich abmelden, erfordert jede neue Komponente, dass Sie sich fragen „sollte dies gecacht werden?" und dann daran denken, die richtige Direktive hinzuzufügen, wenn die Antwort nein ist. Wenn No-Caching die Standardeinstellung ist und Sie sich anmelden, denken Sie nur über Caching nach, wenn Sie es aktiv möchten. Das ist ein grundlegend anderes – und besseres – mentales Modell.

Was hat sich in Next.js 15 und 16 geändert

Next.js 15 war die große philosophische Verschiebung. Das Team kehrte die Standardeinstellungen um:

Verhalten Next.js 14 Next.js 15 Next.js 16
fetch() in Server Components Standardmäßig gecacht Nicht standardmäßig gecacht Nicht standardmäßig gecacht
Route Handlers (GET) Standardmäßig gecacht Nicht standardmäßig gecacht Nicht standardmäßig gecacht
Client Router Cache 30s (dynamisch) / 5min (statisch) 0s für Page Segments 0s Standard, konfigurierbar
Full Route Cache Für statische Routes aktiviert 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, stabilisiert dies als cacheComponents-Konfigurationsoption und die zugehörige "use cache"-Direktive, zusammen mit cacheLife zum Definieren benutzerdefinierter Cache-Profile und cacheTag für gezielte Invalidierung.

Die Schlüsseleinsicht: Caching verlagerte sich von einem impliziten Framework-Verhalten zu einer expliziten Developer-Wahl auf Component-Level. Dies ist ein großer Deal für große Websites.

Understanding cacheComponents

Die cacheComponents-Funktion in next.config.js ermöglicht Component-Level-Caching durch die "use cache"-Direktive. Hier ist das grundlegende Setup:

// next.config.js (Next.js 16)
const nextConfig = {
  experimental: {
    cacheComponents: true,
  },
};

module.exports = nextConfig;

Nach der Aktivierung können Sie "use cache" oben in jede async Server Component, Server Action oder sogar eine Layout-Datei 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'); // custom cache profile
  cacheTag(`product-${params.slug}`);

  const product = await fetchProduct(params.slug);
  
  return (
    <div>
      <h1>{product.name}</h1>
      <ProductDetails product={product} />
      <DynamicPricing productId={product.id} /> {/* This component is NOT cached */}
    </div>
  );
}

cacheLife Profile

Hier wird es für große Websites interessant. Sie definieren benannte Cache-Profile in next.config.js:

const nextConfig = {
  experimental: {
    cacheComponents: true,
    cacheLife: {
      products: {
        stale: 300,      // serve stale for 5 minutes
        revalidate: 3600, // revalidate after 1 hour
        expire: 86400,    // hard expire after 24 hours
      },
      editorial: {
        stale: 3600,
        revalidate: 86400,
        expire: 604800,   // 7 days
      },
      navigation: {
        stale: 86400,
        revalidate: 604800,
        expire: 2592000,  // 30 days
      },
    },
  },
};

Das Drei-Ebenen-Modell (stale, revalidate, expire) ordnet sich schön Stale-While-Revalidate-Semantik zu. Während des stale-Fensters wird gecachter Inhalt sofort bereitgestellt. Nach stale aber vor expire wird eine Hintergrund-Revalidierung eingeleitet. Nach expire ist der Cache-Eintrag weg.

cacheTag für Invalidierung

Die cacheTag-Funktion ersetzt das alte revalidateTag-Muster durch etwas Besser-Zusammensetzbares:

import { revalidateTag } from 'next/cache';

// In a webhook handler or server action:
export async function handleProductUpdate(productSlug: string) {
  revalidateTag(`product-${productSlug}`);
  revalidateTag('product-listing'); // invalidate listing pages too
}

Dieser Teil hat sich von Next.js 15 nicht viel geändert, funktioniert aber viel besser mit cacheComponents, da Sie spezifische gecachte Komponenten taggen, anstatt zu versuchen, undurchsichtige Framework-Level-Caches zu invalidieren.

Next.js 16 cacheComponents: Migrierung von 91.000 Seiten vom App Router Caching - Architektur

Unsere Migrationsstrategie für 91.000 Seiten

Wir haben das nicht auf einmal gemacht. Mit 91.000 Seiten über 14 Locales wäre eine Big-Bang-Migration rücksichtslos gewesen. Hier ist, wie wir es aufgeteilt haben:

Phase 1: Upgrade auf Next.js 16, Keine Cache-Änderungen (Woche 1-2)

Wir haben von Next.js 14.2 auf 16.0 aktualisiert, ohne cacheComponents zu aktivieren. Dies allein veränderte das Verhalten, weil Fetch-Anfragen nicht mehr standardmäßig gecacht wurden. Wir erwarteten TTFB-Regressionen und wir bekamen sie:

  • Durchschnittliche TTFB stieg von 180ms auf 340ms auf Produktseiten
  • Origin Server Load stieg um ~60% (unser Sanity CDN hielt standhaft, aber unsere benutzerdefinierten API-Endpunkte nicht)
  • ISR-Revalidierung wurde tatsächlich schneller, weil es weniger Cache-Status zu verwalten gab

Dies bestätigte, was wir verdächtigten: Wir waren stark auf implizites Caching angewiesen, und viele unserer Seiten brauchten wirklich Caching – nur explizites, absichtliches Caching.

Phase 2: Audit und Klassifizierung von Seiten (Woche 3)

Wir kategorisierten jede Route in unserer App:

Seitentyp Anzahl Cache-Strategie cacheLife Profil
Produktdetailseiten 42.000 Cache mit Product Tag products (5min veraltend / 1h Revalidierung)
Kategorielisten-Seiten 3.200 Cache mit Kategorie-Tag products (5min veraltend / 1h Revalidierung)
Redaktionelle/Blog-Seiten 8.400 Aggressives Caching editorial (1h veraltend / 24h Revalidierung)
Lokalisierte Varianten 31.647 Gleich wie Basis-Seite Geerbt von Basis
Konto/Dynamische Seiten 6.000 Kein Cache N/A

Phase 3: cacheComponents aktivieren, Direktiven hinzufügen (Woche 4-6)

Wir aktivierten das Flag und begannen, "use cache"-Direktiven hinzuzufügen. Die Schlüsseleintscheidung: Wir haben auf Page-Level für die meisten Routes gecacht, aber auf Component-Level für Seiten mit gemischtem statischem/dynamischem Inhalt.

Für Produktseiten wurde die Produktinfo und Bilder gecacht, aber die Pricing-Komponente und der Bestandsstatus waren 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
// NO "use cache" directive -- always fresh

export async function DynamicPricing({ productId }: { productId: string }) {
  const pricing = await getPricing(productId); // hits pricing API every request
  
  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 verdrahteten unsere Sanity-Webhooks neu, um revalidateTag mit den richtigen Tags aufzurufen. Dies war tatsächlich 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 Sie eine ähnliche Migration durchführen, ist dies das praktische Playbook, das wir empfehlen würden (und das wir jetzt für Next.js-Entwicklungsprojekte bei Social Animal verwenden):

Schritt 1: Aktivieren Sie das Flag

// next.config.js
module.exports = {
  experimental: {
    cacheComponents: true,
    cacheLife: {
      // Start with sensible defaults
      default: {
        stale: 60,
        revalidate: 900,
        expire: 86400,
      },
    },
  },
};

Schritt 2: Finden Sie Ihre Hot Paths

Verwenden Sie Ihre Analytik, um die Seiten zu identifizieren, die den meisten Traffic erhalten und wo TTFB am meisten zählt. Für uns waren es Kategorieseiten (hoher Traffic, relativ stabiler Inhalt) und Produktseiten (hoher Traffic, moderat dynamischer Inhalt).

Schritt 3: Fügen Sie Top-Down `"use cache"` hinzu

Beginnen Sie mit Layouts. Wenn Ihr Root-Layout Navigationsdaten abruft, cachen Sie das zuerst – dies ist die höchste Auswirkung, niedrigste Risiko-Änderung:

// app/layout.tsx
// Note: "use cache" on layouts caches the layout shell
// Child pages still render independently

import { Navigation } from '@/components/Navigation';

export default async function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Navigation /> {/* This component has its own "use cache" */}
        {children}
      </body>
    </html>
  );
}

Schritt 4: Richten Sie Monitoring ein

Wir verwendeten Vercel's eingebaute Analytik plus benutzerdefiniertes Logging, um Cache-Hit-Raten zu verfolgen. In der ersten Woche nach der Aktivierung von cacheComponents betrug unsere Cache-Hit-Rate nur 34%. Nach dem Tuning von stale-Dauern kletterte sie auf 78%.

Performance-Ergebnisse und Benchmarks

Hier sind die realen Zahlen nach der vollständigen Migration, gemessen über einen 30-Tage-Zeitraum auf Vercel's 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 Function-Aufrufe/Tag 2.4M 3.8M 1.1M
Monatliche Vercel-Rechnung ~$840 ~$1.200 ~$520
Cache-Hit-Rate Unbekannt (implizit) N/A 78%
Vorfälle mit veralteten Inhalten (#cache-crimes) 8-12/Woche 0 1-2/Monat

Die Build-Zeit-Verbesserung verdient Erklärung. Mit cacheComponents zogen wir von der Generierung aller 91.000 Seiten zur Build-Zeit weg. Stattdessen haben wir nur die Top 5.000 Seiten (nach Traffic) statisch generiert und ließ den Rest bei Bedarf mit Caching generieren. Die cacheComponents-Direktive bedeutete, dass diese On-Demand-Seiten nach dem ersten Besuch gecacht wurden, mit cacheLife kontrollierend Frische.

Die Vercel-Rechnungskürzung war signifikant. Weniger Function-Aufrufe (wegen explizitem Component-Caching) plus kürzere Build-Zeiten bedeuteten echte Kostenersparnisse. Diese ~$320/Monat Reduktion amortisiert sich selbst.

Fallstricke und Gotchas

Serialisierungsgrenzen

Die "use cache"-Direktive erzeugt eine Serialisierungsgrenze. Alles, das als Props an eine gecachte Komponente übergeben wird, muss serialisierbar sein. Wir hatten mehrere Komponenten, die Rückruffunktionen oder React-Elemente als Props erhielten – diese brachen sofort. Der Fix war die Umstrukturierung, um stattdessen Zusammensetzungsmuster zu verwenden:

// ❌ This breaks with "use cache"
"use cache";
export async function ProductCard({ product, onAddToCart }) {
  // onAddToCart is a function -- not serializable!
}

// ✅ This works
"use cache";
export async function ProductCard({ product }) {
  return (
    <div>
      <h2>{product.name}</h2>
      {/* AddToCart is a Client Component, not cached */}
      <AddToCartButton productId={product.id} />
    </div>
  );
}

Dynamische Parameter und Cache-Key-Explosion

Mit 91.000 Seiten, jede mit eindeutigen Parametern, ist der Cache-Key-Raum enorm. Wir trafen Vercel's Edge-Cache-Limits in der ersten Woche und mussten strategischer sein, welche Seiten lange expire-Werte bekamen. Low-Traffic Long-Tail-Seiten bekamen kürzere Cache-Dauern.

Die `Date.now()`-Falle

Jede Komponente mit "use cache", die Date.now() oder new Date() innerhalb der gecachten Funktion aufruft, wird diesen Zeitstempel cachen. Wir fanden dies in einer „Zuletzt aktualisiert"-Anzeige, die die gleiche Zeit für Stunden anzeigte. Der Fix: Zeitempfindliche Logik in eine Client-Komponente oder eine ungecachte Server-Komponente verschieben.

Verschachtelte Cache-Grenzen

Wenn Sie gecachte Komponenten in andere gecachte Komponenten verschachteln, 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 Sie cacheComponents verwenden sollten und wann nicht

Verwenden Sie es wenn:

  • Sie mehr als ein paar hundert Seiten haben und ISR Build-Zeiten sind schmerzhaft
  • Ihr Inhalt hat klare Frische-Anforderungen, die nach Abschnitt variieren
  • Sie detaillierten Kontrolle über was gecacht vs. immer-frisch ist brauchen
  • Sie auf Vercel oder einer Plattform laufen, die die Next.js Cache-Layer unterstützt
  • Sie Infrastruktur-Kosten auf High-Traffic-Websites reduzieren möchten

Verwenden Sie es nicht wenn:

  • Ihre Website klein genug ist, dass vollständiges SSG funktioniert
  • Jede Seite ist vollständig dynamisch (benutzer-spezifischer Inhalt überall)
  • Sie nicht auf einer Hosting-Plattform sind, die Next.js Caching-Infrastruktur unterstützt
  • Ihr Team ist neu bei Next.js – werden Sie zuerst mit den Grundlagen vertraut

Wenn Sie evaluieren, ob Ihr Projekt diese Ebene der Caching-Kontrolle braucht, oder ob ein anderes Framework wie Astro für Ihre inhaltsreiche Website besser passt, ist das wert, vor dem Eintrag in eine Migration durchzudenken.

Für Projekte, wo Inhalt aus mehreren Headless CMS-Quellen kommt, funktioniert das cacheTag-System in Next.js 16 wunderbar mit Headless CMS-Architekturen – jeder Inhaltstyp bekommt seinen eigenen Invalidierungs-Kanal.

FAQ

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 ermöglicht. Es lässt Sie explizit markieren, welche Komponenten gecacht werden sollten, 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 cachet ganze Seiten und revalidiert sie basierend auf einem zeitbasierten Schedule. cacheComponents lässt Sie einzelne Komponenten innerhalb einer Seite cachen, jede mit verschiedenen Cache-Lebensdauern. Eine einzelne Seite kann einen Header mit 24-Stunden-Cache, Produktinfo mit 1-Stunden-Cache und Pricing haben, das niemals gecacht wird. 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 am besten auf Vercel, weil die Caching-Infrastruktur eng integriert ist. Self-Hosted Next.js Deployments können cacheComponents mit dem Dateisystem-Cache-Adapter verwenden, aber Sie bekommen nicht die Edge-Distribution-Vorteile. Plattformen wie Netlify und Cloudflare fügen Unterstützung hinzu, aber ab Mitte 2025 bleibt Vercel die vollständigste Implementierung.

Wie invalidiere ich gecachte Komponenten in Next.js 16? Sie verwenden cacheTag() innerhalb Ihrer gecachten Komponente, um Tags zuzuweisen, dann rufen Sie revalidateTag('tag-name') aus einer Server Action, Route Handler oder Webhook-Endpunkt auf. Dies invalidiert alle gecachten Komponenten mit diesem Tag. Es ist die gleiche API von Next.js 15, aber es ist nützlicher jetzt, weil Sie spezifische gecachte Komponenten taggen, anstatt zu versuchen, undurchsichtige Framework-Caches zu invalidieren.

Wird cacheComponents meine Vercel-Rechnung reduzieren? Es kann Kosten signifikant reduzieren. In unserem Fall sanken Function-Aufrufe um 54%, weil gecachte Component-Responses aus der Cache-Layer bedient wurden, anstatt Serverless-Funktionen aufzurufen. Die Build-Zeit-Reduktion spart auch auf Build-Minuten. Ihre Mileage wird variieren basierend auf Traffic-Mustern und Cache-Hit-Raten – überprüfen Sie Vercel's Pricing Calculator mit Ihrer aktuellen Nutzung.

Was passiert, wenn ich "use cache" zu einer Komponente hinzufüge, die nicht-serialisierbare Props erhält? Sie bekommen einen Build-Fehler. Die "use cache"-Direktive erzeugt eine Serialisierungsgrenze, also müssen alle Props serialisierbar sein (Strings, Zahlen, Plain Objects, Arrays). Funktionen, React-Elemente, Klasseninstanzen und andere nicht-serialisierbare Werte werden den Build fehlschlagen lassen. Umstrukturieren Sie Ihre Komponente, um nur Data Props zu akzeptieren und Interaktivität in Kind Client-Komponenten zu handhaben.

Kann ich cacheComponents mit React Server Components aus anderen Frameworks verwenden? Nein. cacheComponents ist ein Next.js-spezifisches Feature, das auf React's Server Components aufbaut. Während die "use cache"-Direktiven-Syntax möglicherweise eines Tages ein React-Standard wird, sind die cacheLife-Profile und das cacheTag-System Next.js APIs. Wenn Sie ein Framework wie Remix oder ein benutzerdefiniertes RSC-Setup verwenden, brauchen Sie verschiedene Caching-Strategien.

Wie lange dauert es, eine große Next.js-Website zu cacheComponents zu migrieren? Für unsere 91.000-Seiten-Website mit einem Team von 4 Entwicklern dauerte die vollständige Migration 7 Wochen einschließlich Testing und Monitoring. Eine kleinere Website (unter 10.000 Seiten) mit einem einfacheren Datenmodell könnte es wahrscheinlich in 1-2 Wochen schaffen. Die tatsächlichen Code-Änderungen sind unkompliziert – die Zeit geht ins Auditing Ihrer Caching-Anforderungen, Testing von Invalidierungs-Flows und Monitoring von Cache-Hit-Raten nach dem Deployment. Wenn Sie es nicht allein gehen möchten, kontaktieren Sie uns – wir haben das ein paar Mal gemacht.