Datenabruf und Caching in Next.js (App Router)

1. Übersicht

Der Next.js App Router speichert Fetch-Ergebnisse automatisch auf dem Server zwischenspeichert, um die Backend-Last zu reduzieren und Seiten schneller zu machen. Aber dieser Caching-Mechanismus unterscheidet sich stark vom Standard-HTTP-Caching, das du vielleicht kennst, und wenn du nicht genau verstehst, wie er funktioniert, wirst du leicht auf Probleme wie “alte Daten aktualisieren sich nicht” oder das Gegenteil “jede Seite wird von vorne neu gerendert, furchtbar langsam” stoßen.

Dieser Artikel behandelt die verschiedenen Caching-Schichten in Next.js: das Caching der fetch-Funktion, Revalidierung nach Zeit, Revalidierung nach Tags und Dynamic Rendering. Danach wirst du wissen, wie du die richtige Fetch-Strategie für jeden Seitentyp (statisch, ISR, dynamisch) wählst und wie du debuggen kannst, wenn der Cache mal “stur” ist.

Hinweis: Dieser Artikel gilt für den App Router (Next.js 13+, empfohlen 14/15). Der Pages Router nutzt einen anderen Mechanismus (getStaticProps, getServerSideProps), der nicht in den Umfang dieses Artikels fällt.

2. Die vier Caching-Schichten von Next.js

Next.js hat 4 verschiedene Caching-Mechanismen, die leicht verwechselt werden, wenn man sie zusammenwerft:

Mechanismus Ort Zweck
Request Memoization Server, während eines Renders Kombiniert identische fetch-Aufrufe in derselben Request
Data Cache Server, zwischen Requests Speichert fetch-Ergebnisse langfristig, kann revalidiert werden
Full Route Cache Server, zur Build-Zeit Speichert HTML + RSC Payload für statische Routes
Router Cache Client, in der Session Speichert Navigationsergebnisse für schnellere Vor-/Zurück-Navigation

Dieser Artikel konzentriert sich auf den Data Cache, da dies der Caching-Mechanismus ist, den du direkt über die Optionen der fetch-Funktion steuern kannst.

3. Standard Fetch: Cache “auf unbegrenzte Zeit”

In Server Components wird fetch von Next.js so angepasst, dass es sich standardmäßig selbst zwischenspeichert:

// app/products/page.tsx
async function getProducts() {
  const res = await fetch("https://api.shop.example.com/products");
  if (!res.ok) throw new Error("Fehler beim Abrufen der Produkt-API");
  return res.json();
}

export default async function ProductsPage() {
  const products = await getProducts();
  return (
    <ul>
      {products.map((p: { id: string; name: string }) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}
  • fetch ohne cache-Option → Next.js nutzt standardmäßig force-cache.
  • Das Ergebnis wird im Data Cache gespeichert, von allen nachfolgenden Requests gemeinsam genutzt, und läuft nicht automatisch ab, es sei denn, du revalidierst es.
  • Diese Seite wird in den Full Route Cache aufgenommen: statisches HTML wird beim Build erstellt und wie eine statische Seite ausgeliefert.

Das ist der Grund, warum viele Menschen überrascht sind: Sie aktualisieren Daten im Backend, aber die Seite ändert sich nicht — weil Next.js das als statische Daten behandelt.

4. Cache mit der cache-Option steuern

Wenn du immer neue Daten abrufen möchtest, nutze no-store:

async function getOrderStatus(orderId: string) {
  const res = await fetch(
    `https://api.shop.example.com/orders/${orderId}`,
    { cache: "no-store" }
  );
  return res.json();
}
  • cache: "no-store" deaktiviert vollständig den Data Cache für diesen Request.
  • Die Route mit diesem Fetch wird als dynamisch markiert und mit jedem Request neu gerendert.
  • Ideal für sich ständig ändernde Daten: Bestellungen in Bearbeitung, Echtzeitpreise, Dashboards.

Hinweis: Wenn nur ein einziger fetch in einer Seite no-store verwendet, wird die ganze Seite zu Dynamic Rendering — auch wenn andere fetch-Aufrufe auf derselben Seite weiterhin force-cache nutzen.

5. Revalidierung nach Zeit (ISR)

Wenn du Cache möchtest, aber nach N Sekunden automatisch aktualisieren (Incremental Static Regeneration), nutze next.revalidate:

async function getCategories() {
  const res = await fetch(
    "https://api.shop.example.com/categories",
    { next: { revalidate: 300 } } // 5 Minuten
  );
  return res.json();
}
  • revalidate: 300 bedeutet: die Daten gelten nach 300 Sekunden als “veraltet”.
  • Der erste Request nach Ablauf der 300 Sekunden erhält immer noch die alten Daten (stale-while-revalidate), aber Next.js triggert einen neuen Fetch im Hintergrund.
  • Der nächste Request erhält dann die neuen Daten.

Du kannst das auf die ganze Route anwenden, indem du eine Variable am Anfang der Datei exportierst:

// app/blog/[slug]/page.tsx
export const revalidate = 300;

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const res = await fetch(`https://api.blog.example.com/posts/${params.slug}`);
  return <article>{/* ... */}</article>;
}
  • export const revalidate gilt für alle Fetches in der Route, solange diese keinen eigenen revalidate-Wert definieren.
  • Wenn ein spezieller Fetch einen anderen next.revalidate-Wert definiert, gewinnt dieser Wert für diesen Fetch.

6. Revalidierung nach Tag: revalidateTag

Zeitbasierte Revalidierung ist gut für regelmäßig ändernde Inhalte, aber nicht ideal, wenn du sofort aktualisieren möchtest, wenn sich Daten ändern (z.B. wenn ein Admin gerade Produktpreise aktualisiert). In diesem Fall verwendest du Cache-Tags.

// app/products/[id]/page.tsx
async function getProduct(id: string) {
  const res = await fetch(
    `https://api.shop.example.com/products/${id}`,
    { next: { tags: [`product-${id}`] } }
  );
  return res.json();
}

Wenn ein Admin ein Produkt aktualisiert, rufe revalidateTag in einem Route Handler oder Server Action auf:

// app/actions/update-product.ts
"use server";

import { revalidateTag } from "next/cache";

export async function updateProduct(id: string, formData: FormData) {
  await fetch(`https://api.shop.example.com/products/${id}`, {
    method: "PATCH",
    body: formData,
  });

  revalidateTag(`product-${id}`); // lösche Cache mit diesem Tag
}
  • tags weist ein “Label” dem Cache-Eintrag des Fetches zu.
  • revalidateTag(tag) löscht alle Data Cache Einträge mit diesem Tag, egal welche Route sie nutzt.
  • Der nächste Zugriff ruft neue Daten ab und speichert sie mit dem Tag.

Das ist der mächtigste Mechanismus, da er die Logik “wann invalidieren” von “welche Route zeigt die Daten” trennt — ähnlich wie Cache-Tagging in Spring Cache oder Redis.

7. Revalidierung nach Pfad: revalidatePath

Wenn du keine Tags verwendest, kannst du direkt nach Route-Pfad revalidieren:

"use server";

import { revalidatePath } from "next/cache";

export async function publishPost(slug: string) {
  // ... rufe Publish-API auf ...
  revalidatePath(`/blog/${slug}`);   // nur diese Seite
  revalidatePath("/blog", "page");    // oder die gesamte Liste
}
  • revalidatePath("/blog/[slug]") löscht den Full Route Cache + Data Cache genau dieser Route.
  • Der zweite Parameter ("page" oder "layout") entscheidet, ob auch verschachtelte Routes, die dieses Layout verwenden, revalidiert werden.
  • Anders als revalidateTag: revalidatePath ist an eine bestimmte Route gebunden, während Tags über mehrere Routes verwendet werden können.

Hinweis: revalidateTag/revalidatePath können nur in Server Actions oder Route Handlers ausgeführt werden, nicht direkt von Client Components aufgerufen.

8. Dynamic Rendering: Wenn Caching nicht mehr sinnvoll ist

Einige APIs zwingen Next.js automatisch, eine Route als dynamisch zu markieren und den Full Route Cache zu überspringen:

import { cookies, headers } from "next/headers";

export default async function AccountPage() {
  const sessionToken = (await cookies()).get("session")?.value;
  const userAgent = (await headers()).get("user-agent");

  const res = await fetch("https://api.shop.example.com/me", {
    headers: { Authorization: `Bearer ${sessionToken}` },
    cache: "no-store",
  });

  return <div>Hallo, {(await res.json()).name}</div>;
}
  • Der Aufruf von cookies() oder headers() markiert automatisch die Route als dynamisch — logisch, da die Daten von einem bestimmten Benutzer abhängen.
  • Kombiniere mit cache: "no-store", um sicherzustellen, dass die Daten immer frisch für die Session sind.

Erzwinge Dynamic explizit (z.B. wenn du sicher weißt, dass man nicht cachen kann):

export const dynamic = "force-dynamic";

9. Route Handlers haben auch eigenes Caching

Route Handler (app/api/.../route.ts) mit der Methode GET folgen den gleichen Cache-Regeln wie Server Components:

// app/api/stock/route.ts
export const revalidate = 60;

export async function GET() {
  const res = await fetch("https://api.shop.example.com/stock");
  const data = await res.json();
  return Response.json(data);
}
  • Wenn GET kein Request-Objekt nutzt (nicht request.url abfragt, um Query-Parameter zu lesen), dann kann es statisch gecacht werden.
  • Wenn der Handler request.headers, request.cookies oder andere Methoden als GET (POST, PUT…) verwendet → immer dynamisch, kein Cache.

10. Client Components: Caching gehört zu anderen Bibliotheken

Das Next.js fetch-Caching gilt nur auf dem Server. In Client Components machst du Fetches selbst mit useEffect oder nutzt eine separate Cache-Bibliothek wie SWR:

npm install swr
"use client";

import useSWR from "swr";

const fetcher = (url: string) => fetch(url).then((r) => r.json());

export function CartBadge({ userId }: { userId: string }) {
  const { data, isLoading } = useSWR(`/api/cart/${userId}`, fetcher, {
    refreshInterval: 10_000, // alle 10 Sekunden abfragen
  });

  if (isLoading) return <span>...</span>;
  return <span>{data.itemCount} Artikel</span>;
}
  • SWR hat Caching, Deduplizierung, Revalidierung bei Focus — völlig unabhängig vom Server Data Cache.
  • Nutze das, wenn du auf dem Client ständig aktualisierte Daten brauchst, aber nicht die ganze Seite neu laden möchtest.

11. Tests für Cache-Key-Logik schreiben

Das Caching-System von Next.js selbst ist schwer direkt unit-test-bar, aber die Logik zum Erstellen von Tags/Pfaden ist normalerweise wert, isoliert getestet zu werden — um Tippfehler in Tags zu vermeiden, die revalidateTag nicht treffen.

// lib/cache-keys.ts
export function productTag(id: string): string {
  return `product-${id}`;
}

export function categoryTag(id: string): string {
  return `category-${id}`;
}
// lib/cache-keys.test.ts
import { describe, it, expect } from "vitest";
import { productTag, categoryTag } from "./cache-keys";

describe("cache-keys", () => {
  it("erstellt Produkt-Tags im richtigen Format", () => {
    expect(productTag("sku-123")).toBe("product-sku-123");
  });

  it("Tags für Produkte und Kategorien kollidieren nicht", () => {
    const id = "42";
    expect(productTag(id)).not.toBe(categoryTag(id));
  });
});
  • Indem du die Tag-Generierungsfunktionen in ein separates Modul auslagerts, stellst du sicher, dass sowohl der Ort, wo du fetch aufrufst (Tag anbringst), als auch wo du revalidateTag aufrufst (Tag löschst), immer das gleiche Format verwenden.
  • Das ist ein sehr häufiger praktischer Fehler: eine Seite schreibt product-${id}, eine andere schreibt products-${id} → Revalidierung trifft das Tag nie.
✓ cache-keys > erstellt Produkt-Tags im richtigen Format
✓ cache-keys > Tags für Produkte und Kategorien kollidieren nicht

12. Häufige Fehler

  • Vergessen, dass Fetch standardmäßig unbegrenzt gecacht wird: Nach dem Deployment ändert sich der Dateninhalt nicht, weil immer noch der alte Cache von vor dem Build (bei statischem Export) oder vom ersten Fetch (bei Server-Deployment) verwendet wird. Behebe das mit revalidate oder no-store.
  • no-store und Cache in derselben Komponente unnötig mischen: Nur ein Fetch mit no-store macht die ganze Seite dynamisch und zunichte macht den Cache-Vorteil aller anderen Fetches.
  • revalidateTag/revalidatePath außerhalb von Server Action/Route Handler aufrufen: keine Wirkung, Next.js wirft einen Fehler oder ignoriert es stillschweigend (je nach Version).
  • Standard fetch (nicht durch Next.js gepatchte) in Drittanbieter-Bibliotheken verwenden (z.B. SDKs mit axios oder reinem undici): wird nicht vom Data Cache verwaltet, du musst selbst revalidieren (z.B. Cache auf einer anderen Ebene oder einen Wrapper um Standard-fetch schreiben).
  • Nach konkreten Entitäten nicht taggen: Ein zu generisches Tag ("products" für alle Produkte) führt dazu, dass ein kleines Update den gesamten Cache ungültig macht und verliert den Vorteil des Cache pro Item.

Wann NICHT cachen: personalisierte Daten pro Session (Einkaufswagen, Kontostand, Benachrichtigungen), Echtzeitdaten (Aktienkurse, Zahlungsstatus) oder irgendwelche Responses, die von cookies()/headers() abhängen. Diese Fälle sollten von Anfang an no-store oder force-dynamic sein, versuche nicht, Cache zu erzwingen und ständig zu revalidieren — es ist sowohl kompliziert als auch nicht der wahren Natur der Daten entsprechend.

13. Fazit

Das Caching in Next.js App Router hat viele Schichten, aber der Teil, den du am meisten direkt steuern kannst, ist der Data Cache über die Optionen der fetch-Funktion: cache, next.revalidate, next.tags. Schnellwahlregel: Daten ändern sich selten → revalidate nach Zeit; Daten ändern sich bei bestimmten Ereignissen (Admin-Update) → tags anbringen und revalidateTag verwenden; Daten sind benutzerspezifisch oder Echtzeit → no-store/force-dynamic. Wenn du diese drei Gruppen gut verstehst, wirst du selten mehr raten müssen, “warum sich die Daten nicht aktualisieren”.

nextjsreactcachingapp-routerserver-components