Design wechseln

React Server Components Performance-Optimierung: Datenabruf und Caching in der Praxis

Easton editorial illustration: React Server Component tree on a performance bench transforming a request waterfall into parallel streaming slabs

Wenn Ihre RSC-Seiten noch 300–500 ms TTFB haben, nutzen Sie vermutlich nur etwa 30 % des Performance-Potenzials. Messdaten zeigen: Mit der richtigen Streaming-Architektur sinkt TTFB auf 45 ms – kein Zauber, sondern das Ergebnis aktivierten Streamings bei React Server Components.

Letztes Jahr habe ich einem E-Commerce-Team bei der Optimierung einer Produktdetailseite geholfen und denselben Fehler gemacht. Sie nutzten den Next.js App Router, TTFB blieb stabil bei etwa 380 ms. Die Ursache: verschachtelte Komponenten holten Daten jeweils selbst – ein klassischer Waterfall: Produktinfo wartet auf Bewertungen, Bewertungen auf Preis, Preis auf Lagerprüfung. Neun Sekunden weißer Bildschirm.

Dieser Artikel zeigt, wie Sie das lösen. Ich vergleiche 4 Waterfall-Lösungen, erkläre 5 Cache-APIs und liefere eine direkt kopierbare Konfigurationsvorlage. Von 450 ms auf 45 ms TTFB – oft reichen ein paar Suspense-Grenzen an der richtigen Stelle.


Das Waterfall-Problem: der größte RSC-Performance-Killer

Ein reales Szenario: Sie öffnen eine Produktdetailseite. Zuerst der Produktname, dann 3 Sekunden warten auf den Preis, weitere 5 Sekunden auf die Bewertungen. Nutzererfahrung? Katastrophal.

Das ist das Waterfall-Problem. Verschachtelte Komponenten holen Daten nacheinander statt parallel. Der Datenabruf in React Server Components blockiert standardmäßig synchron – jede Anfrage mit await stoppt das Rendering, sofern Sie nicht Suspense verwenden.

Zwei Formen des Waterfalls

Erste Form: Server-interner Waterfall. Innerhalb einer Seite holt die Elternkomponente Daten, rendert dann die Kindkomponente, die wiederum eigene Daten abruft. Typischer Code:

// Waterfall-Beispiel – problematischer Code
async function ProductPage({ id: string }) {
  // Erste Anfrage: 1 Sekunde
  const product = await db.getProduct(id);

  // Diese Anfragen starten erst nach dem Rendern der Kindkomponenten
  return (
    <div>
      <ProductDetails product={product} />
      <ProductPrice id={id} />      {/* intern await getPrice(id), 3 Sekunden */}
      <ProductReviews id={id} />    {/* intern await getReviews(id), 5 Sekunden */}
    </div>
  );
}

// ProductPrice.tsx
async function ProductPrice({ id }) {
  const price = await getPrice(id);  // läuft erst nach dem Eltern-Render
  return <span>{price}</span>;
}

Gesamtzeit? 9 Sekunden. Der Nutzer starrt 9 Sekunden auf eine leere Seite.

Zweite Form: Client-Server-Waterfall. Client-Komponenten fragen den Server an, der wiederum die Datenbank abfragt. Subtiler – React DevTools Profiler zeigt es. Eine Variante des N+1-Problems.

Waterfall erkennen

Öffnen Sie den React DevTools Profiler und zeichnen Sie einen Seitenaufruf auf. Eine treppenförmige Anfrageverteilung auf der Zeitachse – jede Anfrage wartet auf die vorherige – bedeutet Waterfall.

Alternativ: Network-Panel im Browser. Starten die Datenanfragen nacheinander statt gleichzeitig, ist das Problem klar.

Viele Entwickler glauben, RSC bringe automatisch Performance-Gewinn. Laut SitePoint-Bericht 2026 nutzen die meisten Teams nur etwa 30 % des Potenzials – weil Waterfalls unbehandelt bleiben.


Vier Lösungen im Vergleich: von grob bis elegant

Es gibt vier gängige Ansätze – von einfach bis komplex, von grob bis elegant.

Lösung 1: Paralleler Abruf mit Promise.all

Der direkteste Ansatz: alle Anfragen gleichzeitig starten, Promise.all wartet auf alle.

// Lösung 1: Paralleler Abruf mit Promise.all
async function ProductPage({ id: string }) {
  // Alle Anfragen gleichzeitig starten
  const [product, price, reviews] = await Promise.all([
    getProduct(id),      // 1 Sekunde
    getPrice(id),        // 3 Sekunden
    getReviews(id),      // 5 Sekunden
  ]);

  return (
    <div>
      <ProductDetails product={product} />
      <ProductPriceDisplay price={price} />
      <ProductReviewsList reviews={reviews} />
    </div>
  );
}

Gesamtzeit? 5 Sekunden – bestimmt durch die langsamste Anfrage.

Vorteile: Einfach, geringer Änderungsaufwand.

Nachteile: Nutzer sehen erst nach der langsamsten Anfrage Inhalt. Zudem Datenkopplung: die Elternkomponente muss wissen, welche Daten Kindkomponenten brauchen – verletzt die Unabhängigkeit der Komponenten.

Lösung 2: Suspense-Grenzen isolieren

Datenabhängige Teile mit Suspense umschließen, damit kritische Inhalte zuerst erscheinen.

// Lösung 2: Suspense-Grenzen isolieren
async function ProductPage({ id: string }) {
  const product = await getProduct(id);  // zuerst kritische Daten

  return (
    <div>
      <ProductDetails product={product} />  {/* nach 1 Sekunde sichtbar */}
      
      {/* Nicht-kritische Teile mit Suspense */}
      <Suspense fallback={<PriceSkeleton />}>
        <ProductPrice id={id} />
      </Suspense>
      
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews id={id} />
      </Suspense>
    </div>
  );
}

Nutzererfahrung: nach 1 Sekunde Produktinfo, nach 3 Sekunden Preis, nach 5 Sekunden Bewertungen.

Vorteile: Kritische Inhalte zuerst, bessere wahrgenommene Performance.

Nachteile: Anfragen starten weiterhin nacheinander. ProductPrice und ProductReviews beginnen erst nach dem Eltern-Render – kein echtes Parallelisieren.

Lösung 3: Promise als Props übergeben

Elternkomponente startet alle Anfragen, übergibt Promises als Props. Kindkomponenten awaiten selbst.

// Lösung 3: Promise-Übergabe-Muster
async function ProductPage({ id: string }) {
  // Alle Anfragen sofort starten, ohne await
  const productPromise = getProduct(id);
  const pricePromise = getPrice(id);
  const reviewsPromise = getReviews(id);

  // Nur kritische Daten awaiten
  const product = await productPromise;

  return (
    <div>
      <ProductDetails product={product} />
      
      <Suspense fallback={<PriceSkeleton />}>
        <ProductPrice pricePromise={pricePromise} />
      </Suspense>
      
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews reviewsPromise={reviewsPromise} />
      </Suspense>
    </div>
  );
}

// ProductPrice.tsx – empfängt Promise
async function ProductPrice({ pricePromise }) {
  const price = await pricePromise;  // nutzt vom Eltern gestartetes Promise
  return <span>{price}</span>;
}

Drei Anfragen starten parallel in der Elternkomponente. Kritische Daten nach 1 Sekunde, Preis nach 3, Bewertungen nach 5.

Vorteile: Alle Anfragen parallel, kritische Inhalte zuerst, entkoppelte Daten (Kind empfängt nur Promise).

Nachteile: Komponentenschnittstelle ändern – von id zu Promise.

Lösung 4: React cache() + preload (empfohlen)

React 19 führt die cache()-API ein. Mit dem Preload-Muster die eleganteste Lösung.

// Lösung 4: React cache() + preload
import { cache } from 'react';

// Datenabruf-Funktion mit cache wrappen
const getComments = cache(async (postId: string) => {
  return db.getComments(postId);
});

// Preload-Funktion exportieren
export const preloadComments = (id: string) => {
  void getComments(id);  // ohne await – starten, nicht blockieren
};

// Elternkomponente
async function PostPage({ postId: string }) {
  preloadComments(postId);  // Bewertungen vorladen
  
  const post = await getPost(postId);  // nur kritische Daten awaiten
  
  return (
    <div>
      <PostContent post={post} />
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments postId={postId} />  {/* id direkt nutzen, cache wiederverwendet */}
      </Suspense>
    </div>
  );
}

// Comments.tsx – Schnittstelle unverändert
async function Comments({ postId }) {
  const comments = await getComments(postId);  // nutzt Preload-Promise
  return <CommentList comments={comments} />;
}

Prinzip: cache() memoized innerhalb eines Render-Zyklus automatisch. Preload startet die Anfrage ohne zu warten; beim await in der Kindkomponente wird dasselbe Promise wiederverwendet.

Vorteile:

  • Komponentenschnittstelle unverändert (weiterhin id)
  • Anfragen automatisch memoized, keine Datenkopplung
  • Beim Löschen der Kindkomponente wird Preload offensichtlich überflüssig

Nachteile: cache()-Mechanismus verstehen; versteckte Kopplung beachten (Preload beim Löschen von Comments mit entfernen).

Vergleich der vier Lösungen

LösungGesamtzeitKritische Inhalte sichtbarDatenkopplungÄnderungsaufwand
Sequenziell9s9sKeineKeiner
Promise.all5s5sJaGering
Suspense5s1sKeineGering
Promise-Übergabe5s1sEntkoppeltMittel
cache() + preload1s1sKeineMittel

Die Wahl hängt vom Team ab: schnelle Migration → Lösung 2, neues Projekt → Lösung 4.


Streaming-Architektur: das Geheimnis von 45 ms TTFB

Klassisches SSR: alle Daten abwarten, vollständiges HTML rendern, einmal an den Browser senden. TTFB (Time to First Byte) = Datenabrufzeit plus Renderzeit.

Konkret: Datenbankabfrage 400 ms, Rendering 50 ms, TTFB etwa 450 ms. Der Nutzer wartet fast eine halbe Sekunde auf weißem Bildschirm.

450ms → 45ms
TTFB-Optimierungseffekt

Wie RSC Streaming den Ablauf verändert

Streaming reduziert nichts – es ändert die Reihenfolge, in der Inhalte beim Nutzer ankommen. Statische Teile sofort, dynamische Teile gestreamt.

// Streaming-Architektur-Beispiel
export default async function Dashboard() {
  return (
    <Layout>                          {/* statische Shell, ohne Suspense */}
      <Nav />                         {/* sofort rendern */}
      <Sidebar />                     {/* sofort rendern */}
      
      <Suspense fallback={<ChartSkeleton />}>
        <DynamicChart />               {/* dynamische Daten, gestreamt */}
      </Suspense>
      
      <Suspense fallback={<TableSkeleton />}>
        <DataTable />                  {/* dynamische Daten, gestreamt */}
      </Suspense>
    </Layout>
  );
}

Ablauf im Detail:

  1. T=0 ms: Statische Shell (Layout, Nav, Sidebar) sofort vom CDN-Edge-Cache
  2. T=30–50 ms: Browser rendert Shell, Skeleton sichtbar
  3. T=200 ms: DynamicChart-Daten fertig, Inhalt an Suspense-Grenze gestreamt
  4. T=400 ms: DataTable-Daten fertig, Inhalt gestreamt

TTFB? Etwa 45 ms – Zeit bis die statische Shell gesendet wird.

Rolle von PPR (Partial Prerendering)

PPR ist eine Next.js-15-Funktion, in Next.js 16 standardmäßig aktiv. Statische Teile werden ins CDN vorgerendert, dynamische Teile bleiben gestreamt.

Konfiguration:

// next.config.js – Next.js 15
module.exports = {
  experimental: {
    ppr: true,  // PPR aktivieren
  },
};

// next.config.js – Next.js 16 (Vorschau)
module.exports = {
  experimental: {
    ppr: 'incremental',  // schrittweise aktivieren
    cacheComponents: true,  // neues Cache-Modell
  },
};

Mit PPR wird die statische Shell (Navigation, Layout, Skeleton) im CDN vorgehalten. Beim Aufruf liefert das CDN sofort statisches HTML, dynamische Teile ergänzt der Server per Stream.

Designprinzipien für Suspense-Grenzen

Kernprinzip: Ohne Suspense für Streaming-Blöcke behandelt React die gesamte App als einen großen Block.

Richtig:

  • Statische Teile ohne Suspense: Navigation, Layout, datenunabhängige Skeletons
  • Dynamische Teile mit Suspense: Komponenten mit DB-/API-Abhängigkeit
// Richtiges Beispiel
export default async function Page() {
  return (
    <>
      <Header />                     {/* statisch, ohne Wrapper */}
      <main>
        <Suspense fallback={<HeroSkeleton />}>
          <HeroSection />             {/* dynamisch, mit Wrapper */}
        </Suspense>
        
        <Suspense fallback={<ContentSkeleton />}>
          <MainContent />             {/* dynamisch, mit Wrapper */}
        </Suspense>
      </main>
      <Footer />                      {/* statisch, ohne Wrapper */}
    </>
  );
}

Falsches Beispiel (gesamte Seite blockiert):

// Falsches Beispiel – Suspense vergessen
export default async function Page() {
  const data = await fetchDashboard();  // await blockiert die ganze Seite
  return (
    <>
      <Header />
      <Dashboard data={data} />
      <Footer />
    </>
  );
}

Ohne Suspense-Grenzen gilt die ganze Seite als ein Streaming-Block. TTFB bleibt bei 450 ms.

Performance-Vergleich

Render-ModusTTFBLCPErläuterung
Klassisches SSR~450ms~500msWartet auf alle Daten
RSC (ohne Suspense)~450ms~500msEntspricht klassischem SSR
RSC Streaming~45ms~200msStatische Shell sofort
RSC + PPR~30ms~150msStatische Shell im CDN gecacht

Quelle: SitePoint-Bericht 2026; Messwerte variieren je nach Datenquelle und CDN-Konfiguration.


Fünf Cache-APIs im Überblick

Next.js und React bieten fünf Caching-Mechanismen. Die richtige Wahl spart Aufwand; die falsche erzeugt doppelte Anfragen.

1. fetch cache (am häufigsten)

fetch-Anfragen in Server Components werden automatisch memoized. Gleiche URL und Parameter im selben Render-Zyklus → nur eine Anfrage.

// fetch cache Beispiel
async function ProductCard({ id }) {
  // automatisch gecacht, gleiche URL nicht doppelt
  const res = await fetch(`https://api.example.com/products/${id}`, {
    cache: 'force-cache',      // Cache erzwingen (Standard)
    next: {
      revalidate: 3600,         // nach 1 Stunde revalidieren
      tags: ['products'],       // Tag für revalidateTag
    },
  });
  return <Card data={res.json()} />;
}

async function ProductList() {
  // nutzt Cache von oben
  const res = await fetch('https://api.example.com/products', {
    next: { tags: ['products'] },
  });
  return <List data={res.json()} />;
}

Optionen:

  • cache: 'force-cache': Cache bevorzugen (Standard)
  • cache: 'no-store': bei jeder Anfrage neu laden
  • next.revalidate: zeitgesteuerte Revalidierung (Sekunden)
  • next.tags: Tags für manuelles Refresh via revalidateTag

2. React cache() (neu in React 19)

Cached Funktionsaufrufe. Für DB-Abfragen und eigene Datenabruf-Funktionen.

import { cache } from 'react';

// DB-Abfrage mit cache wrappen
export const getUser = cache(async (id: string) => {
  const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);
  return user;
});

// In mehreren Komponenten – automatisch memoized
async function UserProfile({ id }) {
  const user = await getUser(id);
  return <Profile user={user} />;
}

async function UserStats({ id }) {
  const user = await getUser(id);  // nutzt Ergebnis von oben
  return <Stats user={user} />;
}

Hinweis: cache() gilt nur innerhalb eines Render-Zyklus. Für requestsübergreifendes Caching → unstable_cache.

3. unstable_cache (Next.js 14–15)

Persistenter Cache über Requests hinweg. Für teure Berechnungen und geteilte Daten.

import { unstable_cache } from 'next/cache';

// Funktion mit persistentem Cache
export const getPopularProducts = unstable_cache(
  async () => {
    const products = await db.getPopularProducts();
    return products;
  },
  ['popular-products'],           // Cache-Key
  {
    revalidate: 3600,              // nach 1 Stunde revalidieren
    tags: ['products', 'popular'], // mehrere Tags
  }
);

// Verwendung
async function HomePage() {
  const products = await getPopularProducts();
  return <ProductGrid products={products} />;
}

Manuelles Refresh:

import { revalidateTag } from 'next/cache';

// in Server Action oder API Route
async function updateProduct() {
  await db.updateProduct();
  await revalidateTag('products');  // alle products-Tags invalidieren
}

4. use cache (neu in Next.js 16)

Cache-Direktive auf Komponenten- oder Funktionsebene. 'use cache' oben – Ausgabe wird gecacht.

// Funktionsebene 'use cache'
'use cache';
export async function getRecommendations(userId: string) {
  return db.getRecommendations(userId);
}

// Komponentenebene 'use cache'
'use cache';
export async function CachedFooter() {
  const links = await getFooterLinks();
  return <Footer links={links} />;
}

Einsatz: häufig aufgerufene Komponenten, statische Inhalte. Experimentell, in Next.js 16 offiziell unterstützt.

5. revalidatePath / revalidateTag

Manuelles Cache-Refresh.

import { revalidatePath, revalidateTag } from 'next/cache';

// nach Pfad
await revalidatePath('/products');      // alle Caches dieses Pfads
await revalidatePath('/products/[id]', 'page');  // bestimmte Seite

// nach Tag
await revalidateTag('products');        // alle products-Tags

Empfehlung:

  • Präzise Steuerung → revalidateTag (empfohlen)
  • Batch-Refresh → revalidatePath

Cache-API-Vergleich

APICache-BereichPersistentEinsatzVersion
fetch cacheEin RequestKonfigurierbarAPI-AnfragenNext.js 13+
React cache()Ein Render-ZyklusNeinDB-Abfragen, eigene FunktionenReact 19
unstable_cacheÜber RequestsJaTeure Berechnungen, geteilte DatenNext.js 14–15
use cacheFunktion/KomponenteJaHäufig aufgerufene KomponentenNext.js 16
Cache ComponentsKomponenten-AusgabeJamit PPRNext.js 16

Praxis-Konfiguration und typische Fallstricke

Theorie genug – hier die kopierbare Konfiguration.

Vollständige next.config.js

// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    // Next.js 15: PPR aktivieren
    ppr: true,
    
    // Next.js 16: neues Cache-Modell
    // cacheComponents: true,  // in stabiler Version aktivieren
  },
  
  // Performance
  images: {
    formats: ['image/avif', 'image/webp'],
  },
  
  // Output-Optimierung
  output: 'standalone',  // für Docker-Deployment
};

export default nextConfig;

Konvention für Preload-Funktionen

Preload erzeugt leicht versteckte Kopplung. Beim Löschen tiefer Kindkomponenten bleibt Preload oft als toter Code.

Empfehlung: Kommentar über der Preload-Funktion mit Zweck und Abhängigkeit.

// comments.ts
import { cache } from 'react';

const getComments = cache(async (postId: string) => {
  return db.getComments(postId);
});

/**
 * preloadComments: lädt Kommentardaten für Comments-Komponente vor
 * Hinweis: beim Löschen von Comments auch preloadComments entfernen
 */
export const preloadComments = (id: string) => {
  void getComments(id);
};

export async function Comments({ postId }) {
  const comments = await getComments(postId);
  return <CommentList comments={comments} />;
}

Typische Fallstricke

Fallstrick 1: Suspense-Grenze vergessen

Symptom: TTFB bleibt bei 450 ms, kein Streaming-Effekt.

Ursache: ohne Suspense gilt die ganze Seite als ein Block.

Lösung: datenabhängige Komponenten mit Suspense umschließen.

// Vorher
async function Page() {
  const data = await getData();  // blockiert die ganze Seite
  return <Dashboard data={data} />;
}

// Nachher
async function Page() {
  return (
    <Suspense fallback={<DashboardSkeleton />}>
      <Dashboard />
    </Suspense>
  );
}

Fallstrick 2: Preload ohne Verwendung

Symptom: Anfrage läuft, Daten werden nicht genutzt – verschwendete Ressourcen.

Ursache: Kindkomponente gelöscht, Preload bleibt.

Lösung: Preload mit Komponente löschen oder per Kommentar verknüpfen.

Fallstrick 3: Cache-Tag-Konflikte

Symptom: revalidateTag invalidiert zu viel.

Ursache: unterschiedliche Daten teilen denselben Tag.

Lösung: pro Geschäftsbereich eigene Tags.

// Falsch
await fetch(url, { next: { tags: ['data'] } });  // alles mit 'data'
await revalidateTag('data');  // alles invalidiert

// Richtig
await fetch(productsUrl, { next: { tags: ['products'] } });
await fetch(usersUrl, { next: { tags: ['users'] } });
await revalidateTag('products');  // nur products

Fallstrick 4: fetch cache und React cache gemischt

Symptom: dieselben Daten werden zweimal abgerufen.

Ursache: fetch mit 'no-store', React cache() kann nicht wiederverwenden.

Lösung: fetch mit force-cache oder Standard, damit React cache() greift.

// Falsch
const data1 = await fetch(url, { cache: 'no-store' });  // kein Cache
const data2 = await getData();  // React cache, aber keine Wiederverwendung

// Richtig
const data1 = await fetch(url);  // Standard force-cache
const data2 = await getData();  // kann wiederverwenden

Debugging-Tools

  1. React DevTools Profiler: Rendering aufzeichnen, Waterfall erkennen
  2. Next.js-Analyse: next build --experimental-debug für Build-Analyse
  3. Chrome DevTools: Network für Anfrage-Timing, Performance für Render-Zeitpunkt

Kernmetriken:

  • TTFB: Time to First Byte, Ziel < 100 ms
  • LCP: Largest Contentful Paint, Ziel < 2,5 s
  • CLS: Cumulative Layout Shift, Ziel < 0,1

Migrations-Empfehlung

Von bestehendem SSR zu RSC-Streaming:

  1. Waterfall identifizieren: Profiler nutzen, sequenzielle Abrufe finden
  2. Suspense-Grenzen setzen: datenabhängige Komponenten umschließen, kritische Pfade zuerst
  3. Preload ergänzen: cache() + preload für tiefe Komponenten
  4. Cache konfigurieren: fetch-Tags für präzises Refresh

Schrittweise migrieren, nicht alles auf einmal. Zuerst langsamste Seiten, Gewinn messen, dann ausweiten.


Zusammenfassung

Kern in drei Schritten: Waterfall erkennen, Lösung wählen, Streaming-Architektur konfigurieren.

Waterfalls erkennen Sie leicht – verschachtelte Komponenten awaiten nacheinander, treppenförmige Verteilung auf der Zeitachse. Vier Lösungen für unterschiedliche Szenarien: schnelle Migration → Suspense-Grenzen, neues Projekt → React cache() + preload.

Entscheidend ist die Platzierung der Suspense-Grenzen. Statische Teile (Navigation, Layout) ohne Wrapper, dynamische Teile (datenabhängige Komponenten) mit Wrapper. Vergessen Sie das, blockiert die ganze Seite weiter.

Messbarer Gewinn: TTFB von 450 ms auf 45 ms – Faktor 10. Dazwischen liegen oft nur ein paar Suspense-Grenzen und eine Preload-Funktion.

Öffnen Sie Ihr Next.js-Projekt und prüfen Sie verschachtelte unabhängige Datenabrufe. Liegt TTFB noch über 300 ms, umschließen Sie kritische Inhalte mit Suspense. Ihre Nutzer merken den Unterschied sofort.


Referenzen

React Server Components Performance-Optimierung

Vollständige Optimierungsschritte von der Waterfall-Erkennung bis zur Streaming-Architektur

⏱️ Estimated time: 60 min

  1. 1

    Step 1: Waterfall-Problem identifizieren

    React DevTools Profiler für Seitenaufruf nutzen:

    • Chrome DevTools öffnen, Profiler-Tab wählen
    • Aufzeichnen starten, Seite neu laden, warten bis fertig
    • Anfrageverteilung auf der Zeitachse prüfen
    • Treppenmuster = Waterfall-Problem
    • Prüfen, ob TTFB 300 ms überschreitet
  2. 2

    Step 2: Lösung wählen

    Je nach Team-Situation:

    • Schnelle Migration: Lösung 2 (Suspense-Grenzen)
    • Neues Projekt: Lösung 4 (React cache() + preload)
    • Datenkopplung akzeptabel: Lösung 1 (Promise.all)
    • Schnittstelle unverändert: Lösung 4 (empfohlen)
  3. 3

    Step 3: Suspense-Grenzen setzen

    Datenabhängige Komponenten mit Suspense umschließen:

    • Statische Teile ohne Wrapper (Navigation, Layout)
    • Dynamische Teile mit Wrapper (datenabhängige Komponenten)
    • Passende Fallback-Skeletons bereitstellen
    • Kritische Pfade zuerst
  4. 4

    Step 4: React cache() + preload konfigurieren

    React-19-cache()-API nutzen:

    • Datenabruf-Funktionen mit cache wrappen
    • Preload-Funktion exportieren, ohne await
    • Kommentare für Abhängigkeiten setzen
    • Preload beim Löschen der Komponente entfernen
  5. 5

    Step 5: Cache-Strategie konfigurieren

    Passende Cache-API wählen:

    • fetch cache: API-Anfragen (am häufigsten)
    • React cache(): Datenbankabfragen
    • unstable_cache: requestübergreifendes Teilen
    • Tags für präzises Refresh setzen
  6. 6

    Step 6: Performance-Gewinn messen

    Optimierung verifizieren:

    • TTFB-Ziel: &lt; 100 ms
    • LCP-Ziel: &lt; 2,5 s
    • CLS-Ziel: &lt; 0,1
    • Vorher/Nachher-Vergleich der Messdaten

FAQ

Was ist das Waterfall-Problem bei React Server Components?
Das Waterfall-Problem beschreibt verschachtelte Komponenten, die Daten nacheinander abrufen – die Ladezeit summiert sich. Die Elternkomponente holt Daten, rendert Kinder, die wiederum Daten holen; Gesamtzeit = Summe aller Anfragen. React DevTools Profiler zeigt treppenförmige Anfragemuster.
Wie wähle ich zwischen den vier Waterfall-Lösungen?
Je nach Team-Situation:

• Promise.all: Einfach, schneller Fix, aber Datenkopplung
• Suspense-Grenzen: Kritische Inhalte zuerst, gut für Migration
• Promise-Übergabe: Alle Anfragen parallel, wenn Schnittstellen änderbar
• React cache() + preload: Eleganteste Lösung für neue Projekte (empfohlen)
Wo soll ich Suspense-Grenzen platzieren?
Kernprinzip: Statische Teile ohne Suspense, dynamische Teile mit Suspense. Statisch: Navigation, Layout, datenunabhängige Skeletons. Dynamisch: Komponenten mit DB- oder API-Abhängigkeit. Ohne Wrapper blockiert die ganze Seite.
Was ist der Unterschied zwischen den fünf Cache-APIs?
Unterschiedliche Einsatzszenarien:

• fetch cache: API-Anfragen, auto-memoized (Next.js 13+)
• React cache(): DB-Abfragen, Cache pro Render-Zyklus (React 19)
• unstable_cache: Persistent über Requests, teure Berechnungen (Next.js 14–15)
• use cache: Funktions-/Komponenten-Cache (Next.js 16)
• revalidatePath/Tag: Manuelles Cache-Refresh
Wie messe ich den Erfolg der RSC-Performance-Optimierung?
React DevTools Profiler nutzen, Waterfall-Verteilung prüfen. Kernmetriken: TTFB unter 100 ms, LCP unter 2,5 s, CLS unter 0,1. Nach Optimierung kann TTFB von 450 ms auf 45 ms sinken.
Was ist PPR (Partial Prerendering)?
PPR ist eine Next.js-15-Funktion, in Next.js 16 standardmäßig aktiv. Statische Teile werden ins CDN vorgerendert, dynamische bleiben gestreamt. Statische Shell (Navigation, Layout) kommt sofort vom CDN, TTFB kann auf 30 ms sinken. Konfiguration: experimental.ppr = true.
Welche typischen Cache-Konfigurationsfehler gibt es?
Vier häufige Probleme:

• Suspense-Grenze vergessen: Ganze Seite blockiert, TTFB unverändert
• Preload ohne Nutzung: Komponente gelöscht, Preload bleibt, verschwendete Anfragen
• Cache-Tag-Konflikte: revalidateTag invalidiert zu viel
• fetch cache und React cache gemischt: no-store verhindert Wiederverwendung

13 Min. Lesezeit · Veröffentlicht am: 13. Mai 2026 · Aktualisiert am: 14. Juli 2026

Ähnliche Beiträge

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog