Zum Inhalt springen

8.2 Next.js: Server- und Client-Komponenten

Zu Zen-Modus wechseln

Nach dieser Einheit kannst du:

  • Server- und Client-Komponenten unterscheiden, nach klaren Regeln zwischen beiden entscheiden und begründen, warum Geheimnisse nie den Client erreichen.
  • mit einem einfachen Experiment nachweisen, wo eine Komponente gerendert wird.
  • entscheiden, ob ein Datenzugriff mit "use cache" zwischengespeichert oder mit <Suspense> bei jeder Anfrage geladen wird, und den Build-Bericht mit ○ und ◐ lesen.
  • Hydration erklären, einen Hydration-Fehler erkennen und beheben.
  • Seiten so in Server- und Client-Anteile zerlegen, dass die Grenze "use client" so tief wie möglich liegt.

Im Lokal aus Kapitel 5 gibt es zwei Bereiche. In der Küche liegen die Vorräte, die Rezepte und die Lieferantenkonten; dort wird gekocht, und nur der fertige Teller verlässt die Küche. Im Gastraum sitzen die Gäste; dort wird gesalzen, umgerührt und nachbestellt. Niemand trägt das Rezeptbuch in den Gastraum, und niemand kocht am Tisch. Next.js teilt eine Anwendung genauso: Server-Komponenten sind die Küche, Client-Komponenten der Gastraum.

flowchart LR
    accTitle: Server als Küche, Browser als Gastraum
    subgraph server["Server: Küche"]
        direction TB
        data[("Dateien, Datenbank,<br/>Geheimnisse")] --> sc["Server-Komponente<br/>rendert HTML"]
    end
    subgraph browser["Browser: Gastraum"]
        direction TB
        html["fertiges HTML"] --> cc["Client-Komponente<br/>Klicks, State"]
    end
    sc -- "nur das Ergebnis" --> html

Abb. 8.5: Die Daten bleiben in der Küche, in den Gastraum kommt nur der fertige Teller (eigene Darstellung).

Die Frage “Wo läuft mein Code?” begleitet den Rest des Jahres. Diese Einheit klärt, an welchem Ort jede Komponente läuft, wann der Server rendert und wie aus HTML eine bedienbare Seite wird.

  • Server-Komponente: eine Komponente, die nur am Server läuft; der Standard in Next.js.
  • Client-Komponente: eine Komponente, die im Browser läuft und mit "use client" in der ersten Zeile markiert ist.
  • SSR (Server-Side Rendering): Der Server erzeugt fertiges HTML; CSR (Client-Side Rendering): Der Browser erzeugt den Inhalt.
  • Statische Hülle: der Teil einer Seite, den Next.js schon beim Build fertig rendert; der Rest kommt bei der Anfrage nach (Streaming).
  • Hydration: React macht das fertige HTML im Browser bedienbar, indem es Event-Handler und State anhängt.

Next.js führt Komponenten an zwei Orten aus, und der Unterschied ist das Architekturmerkmal des Frameworks. Als Einstieg ordnet das folgende Video die Begriffe ein (Englisch):

Abspielen

Server-Komponenten sind der Standard: Jede Komponente ohne besondere Markierung wird am Server gerendert, und nur das Ergebnis erreicht den Browser. Server-Komponenten dürfen deshalb Dinge, die im Browser undenkbar wären: direkt auf Dateien und später auf Datenbanken zugreifen, mit Geheimnissen wie API-Schlüsseln arbeiten, und sie dürfen async sein:

// app/presets/page.tsx: a server component reading a file
import { readFile } from "node:fs/promises";
export default async function PresetsPage() {
"use cache"; // same for every visitor: cache the result in the static shell
const raw = await readFile("data/presets.json", "utf-8");
const presets: string[] = JSON.parse(raw);
return (
<ul>
{presets.map((preset) => <li key={preset}>{preset}</li>)}
</ul>
);
}

Die Zeile "use cache" erklärt der Abschnitt Statische Hülle, Cache und Streaming: Die Datei ist für alle Besucher gleich, also darf Next.js das Ergebnis zwischenspeichern. Dieses Vorgehen heißt Server-Side Rendering (SSR): Der Server beschafft die Daten, rendert daraus fertiges HTML und schickt es an den Browser, der es sofort anzeigt und anschließend bedienbar macht:

Ablauf des Server-Side Rendering mit Next.js: 1. Der Client schickt eine Anfrage an den Server. 2. Der Server erzeugt die Daten und rendert am Server das HTML. 3. Das fertige, vorgerenderte HTML-Dokument wird an den Client gesendet. 4. Der Browser zeigt die fertige Seite an und hydriert sie, wird also interaktiv.
Abb. 8.6: Server-Side Rendering (SSR). Der Server erzeugt Daten und fertiges HTML und sendet es an den Client, der es sofort anzeigt und anschließend hydriert.

Client-Komponenten laufen im Browser und beginnen mit der Direktive "use client" in der ersten Zeile. Nur sie können, was Interaktivität braucht: Hooks (useState, useEffect), Event-Handler (onClick, onChange) und Browser-APIs (localStorage).

// app/jobs/job-filter.tsx: interactivity needs the client
"use client";
import { useState } from "react";
export function JobFilter({ names }: { names: string[] }) {
const [query, setQuery] = useState("");
const visible = names.filter((n) => n.includes(query));
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ul>{visible.map((n) => <li key={n}>{n}</li>)}</ul>
</>
);
}

Beispielprojekt: 08_nextjs_struktur_routing_examples/ex02_server_and_client

Das Gegenstück zu SSR ist Client-Side Rendering (CSR): Der Server liefert nur ein Gerüst und das JavaScript-Bundle; den Inhalt rendert erst der Browser. Eine reine Vite-Anwendung wie in Kapitel 7 arbeitet so:

Ablauf des Client-Side Rendering mit Next.js: 1. Der Client schickt eine Anfrage für JavaScript und leeres HTML. 2. Der Server stellt minimales HTML und die JavaScript-Dateien bereit. 3. Das minimale, nicht gerenderte HTML-Dokument und das JS-Bundle werden gesendet. 4. Die durch die use-client-Direktive markierte Komponente erzeugt die Daten und rendert das HTML am Client. 5. Das gerenderte, interaktive HTML-Dokument steht im Browser bereit.
Abb. 8.7: Client-Side Rendering (CSR). Der Server liefert nur minimales HTML und das JavaScript-Bundle; das eigentliche Rendern der Inhalte geschieht erst im Browser.

Genauer erklärt: Auch eine Client-Komponente rendert Next.js beim ersten Laden einmal am Server vor, damit der Browser sofort fertiges HTML bekommt. “Client-Komponente” heißt also: Sie darf im Browser laufen und bekommt dort State und Events, nicht: Sie läuft nie am Server.

Die Entscheidungstabelle:

BedarfServerClient
Daten laden (Datei, Datenbank, interne API)jaüber HTTP
Geheimnisse (Schlüssel, Zugangsdaten)janiemals
Hooks, Events, Interaktivitätneinja
Browser-APIs (localStorage, …)neinja

Das Standardmuster kombiniert beide: Die Server-Komponente beschafft die Daten und reicht sie als Props an eine Client-Komponente, im Beispielprojekt die Seite /jobs an JobFilter. Die Grenze ist dabei eine Sicherheitsgrenze: Was eine Client-Komponente als Prop erhält, steht im Browser und damit jedem Besucher offen; Geheimnisse bleiben auf der Server-Seite.

Wie das Zusammenspiel im Code aussieht, führt das folgende Video vor (Englisch):

Abspielen
Auflösung

Server: (a), weil Dateizugriff nur dort geht, und (c), weil der Schlüssel nie in den Browser darf. Client: (b) braucht ein onClick, (d) braucht State, (e) braucht den Hook usePathname(), der den aktuellen Pfad im Browser kennt. Dabei bleibt die Seite Server-Komponente; nur die kleinen interaktiven Bausteine bekommen "use client".

SSR sagt, wo gerendert wird: am Server. Offen bleibt wann. Mit Cache Components (Einheit 8.1) rendert Next.js beim npm run build jede Seite so weit vor, wie es geht. Das Ergebnis ist die statische Hülle: fertiges HTML, das ohne Wartezeit an jeden Besucher geht. Was beim Build noch nicht feststeht, kommt bei der Anfrage nach.

Dafür verlangt Next.js eine ausdrückliche Entscheidung, sobald eine Seite auf etwas wartet, das sich ändern kann: eine Datei oder Datenbank, fetch, die Uhrzeit, den Pfad (params) oder Anfragedaten wie cookies() und headers(). Es gibt zwei Antworten:

  • "use cache": Das Ergebnis ist für alle Besucher gleich und darf zwischengespeichert werden. Es landet in der statischen Hülle und wird nach einer Frist im Hintergrund erneuert; ohne weitere Angabe nach 15 Minuten.
  • <Suspense>: Das Ergebnis muss bei jeder Anfrage frisch sein. In der Hülle steht ein Platzhalter (fallback), der Inhalt wird pro Anfrage gerendert und nachgeliefert (Streaming). loading.tsx aus Einheit 8.1 ist genau so eine Suspense-Grenze um die ganze Seite.

Fehlt beides, bricht npm run build ab: “Next.js encountered uncached or runtime data during prerendering.” Für die Uhrzeit meldet Next.js eigens, dass new Date() einen unbeständigen Wert liefert.

flowchart LR
    accTitle: Statische Hülle, Cache und Streaming
    build["npm run build"] --> shell["statische Hülle:<br/>festes HTML"]
    cache["use cache:<br/>Ergebnis zwischengespeichert"] --> shell
    shell --> req["Anfrage"]
    req --> stream["Suspense:<br/>Inhalt pro Anfrage<br/>nachgeliefert"]

Abb. 8.8: Was gleich bleibt, steht in der Hülle; was frisch sein muss, wird hinter einem Platzhalter nachgeliefert (eigene Darstellung).

Zwei Seiten aus dem Beispielprojekt zeigen den Unterschied, beide geben die Uhrzeit aus, zu der sie gerendert wurden:

// app/frozen/page.tsx: cached, so it becomes part of the static shell
export default async function FrozenPage() {
"use cache";
return <p>Rendered at {new Date().toISOString()}</p>;
}
app/live/page.tsx
import { connection } from "next/server";
import { Suspense } from "react";
async function RenderTime() {
await connection(); // wait for a real request: render per request
return <p>Rendered at {new Date().toISOString()}</p>;
}
export default function LivePage() {
return (
<Suspense fallback={<p>Loading ...</p>}>
<RenderTime />
</Suspense>
);
}

Beispielprojekt: 08_nextjs_struktur_routing_examples/ex03_static_and_dynamic

await connection() sagt Next.js, dass die Komponente auf eine echte Anfrage wartet; erst danach ist new Date() erlaubt. Im Produktionsbetrieb (npm run build, dann npm start) zeigt /frozen bei jedem Neuladen dieselbe Zeit, /live jedes Mal eine neue. Der Build-Bericht markiert das mit Symbolen:

├ ○ /frozen 15m 1y
└ ◐ /live
○ (Static) prerendered as static content
◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content

○ heißt: Die ganze Seite steckt in der Hülle; die Spalten daneben nennen die Frist bis zur Erneuerung (15 Minuten) und bis zum Verfall (ein Jahr). ◐ heißt: Hülle plus nachgelieferter Teil. Auch /jobs/[id] aus Einheit 8.1 ist ◐, weil die ID erst bei der Anfrage feststeht; kennt man die möglichen Werte schon beim Build, kann eine Funktion generateStaticParams sie aufzählen, dann bekommt jede davon eine fertige Seite.

Wo begegnet dir das? Auf Nachrichtenseiten: Kopfzeile, Menü und der Artikel selbst sind für alle gleich und kommen sofort; die Zeile “Angemeldet als …” und die neuesten Kommentare laden einen Augenblick später nach.

Ältere Anleitungen ohne Cache Components verwenden hier export const dynamic = "force-dynamic"; mit Cache Components lehnt der Build diese Angabe ab.

Bei SSR erreicht den Browser zuerst fertiges HTML. Es ist sofort sichtbar und lesbar, aber noch tot: Ein Klick auf einen Button bewirkt nichts, useState hält noch keinen Zustand. Das HTML ist nur die Beschreibung der Oberfläche; Event-Handler und State leben im JavaScript, das erst noch geladen und ausgeführt werden muss.

Diesen zweiten Schritt nennt man Hydration (wörtlich “mit Wasser versorgen”): React lädt im Browser das Client-Bundle, geht das vorhandene HTML durch, hängt an jedes Element die passenden Event-Handler und stellt den Komponentenzustand her. Erst danach ist die Seite bedienbar. React baut das HTML dabei nicht neu auf, sondern übernimmt das vom Server gelieferte und “belebt” es; deshalb ist SSR schnell sichtbar und am Ende voll bedienbar.

sequenceDiagram
    accTitle: Vom fertigen HTML zur bedienbaren Seite
    participant s as Server
    participant b as Browser
    s->>b: fertiges HTML
    Note over b: sichtbar, aber Klicks wirkungslos
    s->>b: Client-Bundle (JavaScript)
    Note over b: Hydration: Handler und State anhängen
    Note over b: bedienbar

Abb. 8.9: Zwischen “sichtbar” und “bedienbar” liegt die Hydration (eigene Darstellung).

Daraus folgen zwei Dinge, die in der Praxis regelmäßig auffallen:

  • Die Reihenfolge ist spürbar. Zwischen “HTML da” und “hydriert” geht ein Klick ins Leere; je größer das Client-Bundle, desto länger.
  • Server- und Client-Ausgabe müssen übereinstimmen. React erwartet, dass das im Browser erzeugte HTML genau zu dem passt, was der Server geschickt hat. Wer beim ersten Rendern etwas verwendet, das am Server und im Browser verschieden ist (Date.now(), Math.random(), localStorage, window), bekommt einen Hydration-Fehler. Next.js zeigt ihn im Entwicklungsmodus an: “Hydration failed because the server rendered text didn’t match the client.”
Auflösung

Im ersten Fall ist die Hydration noch nicht fertig: Das HTML ist da, das JavaScript mit den Handlern noch nicht. Das ist normal und wird mit kleineren Client-Bundles kürzer. Im zweiten Fall unterscheidet sich das HTML aus dem Browser vom HTML des Servers, meist wegen Zeit, Zufall oder localStorage im ersten Rendern; der Wert gehört in einen useEffect.

"use client" markiert nicht eine einzelne Komponente, sondern eine Grenze im Komponentenbaum. Die Direktive steht ganz oben in einer Datei und erklärt: Ab hier, in dieser Komponente und in allem, was sie importiert, sind wir auf der Client-Seite. Deshalb heißt es, "use client" sei “ansteckend”: Der ganze Teilbaum unterhalb der Grenze wird Teil des Client-Bundles, auch Komponenten, die selbst gar kein "use client" tragen.

flowchart TD
    accTitle: Die Client-Grenze im Komponentenbaum
    root["RootLayout<br/>(Server)"] --> page["DashboardPage<br/>(Server)"]
    page --> stats["StatsPanel<br/>(Server)"]
    page --> filter["JobFilter<br/>use client: Grenze"]
    filter --> search["SearchBox<br/>(im Client-Bundle)"]
    filter --> list["ResultList<br/>(im Client-Bundle)"]
    list --> row["ResultRow<br/>(im Client-Bundle)"]

Abb. 8.10: Alles unterhalb von JobFilter landet im Client-Bundle; das Geschwister StatsPanel bleibt am Server (eigene Darstellung).

Zwei Regeln machen den Umgang damit beherrschbar:

  • Die Grenze so tief wie möglich setzen. Je weiter oben "use client" steht, desto größer der Teilbaum, der in den Browser wandert. Steht die Direktive versehentlich im Wurzel-Layout, wird die gesamte Anwendung zur Client-Anwendung, und der Vorteil der Server-Komponenten ist verloren.
  • Server-Inhalte über children durch die Grenze reichen. Angenommen, ein aufklappbarer Rahmen (Accordion, interaktiv, also Client) soll eine Statistik (ServerStats, gehört an den Server) zeigen. Entscheidend ist der Unterschied zwischen importieren und übergeben.

Der naheliegende, aber falsche Weg: Die Client-Komponente importiert die Server-Komponente direkt, mit import { ServerStats } from "./server-stats"; in accordion.tsx und {open && <ServerStats />} im JSX. Durch den import liegt ServerStats unterhalb der Grenze und wird Teil des Client-Bundles. Ein Datei- oder Datenbankzugriff darin würde nicht mehr funktionieren, und das console.log in ServerStats erscheint im Beispielprojekt nach dem Aufklappen in der Browser-Konsole ohne die Markierung “Server”.

Der richtige Weg: Accordion importiert ServerStats nicht, sondern nimmt beliebigen Inhalt als children entgegen. Zusammengesteckt werden beide erst eine Ebene höher, in einer Server-Komponente:

accordion.tsx
"use client";
import { useState } from "react";
export function Accordion({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen(!open)}>Details</button>
{open && children} {/* renders whatever it is given, without knowing it */}
</div>
);
}
// page.tsx (server component)
export default function DashboardPage() {
return (
<Accordion>
<ServerStats /> {/* rendered here, in a server context */}
</Accordion>
);
}

Beispielprojekt: 08_nextjs_struktur_routing_examples/ex05_client_boundary

Weil <ServerStats /> in page.tsx (Server) steht und nicht in accordion.tsx (Client), rendert der Server es fertig und übergibt nur das Ergebnis als children. Accordion steuert lediglich, ob dieser fertige Inhalt angezeigt wird, nicht, wie er entsteht. Im Beispielprojekt loggt ServerStats auf dieser Seite im Terminal.

  1. "use client" im Wurzel-Layout: Die ganze Anwendung wird zur Client-Anwendung.
  2. Hook in einer Server-Komponente: useState ohne "use client" bricht den Build ab (“You’re importing a module that depends on useState into a React Server Component module”); die Direktive gehört in den kleinsten interaktiven Baustein.
  3. Geheimnis als Prop an eine Client-Komponente: Alles, was die Grenze überquert, steht im Browser.
  4. Datenzugriff ohne Entscheidung: Datei, fetch, Datenbank, params oder new Date() ohne "use cache" oder <Suspense>; der Build bricht ab.
  5. "use cache" für persönliche Daten: Was je Besucher verschieden ist (Name, Warenkorb), gehört hinter <Suspense>, sonst sehen alle dasselbe.
  6. export const dynamic aus alten Anleitungen: Mit Cache Components entfallen; der Build meldet, dass die Angabe nicht kompatibel ist.
  7. Zeit oder Zufall im ersten Rendern: Hydration-Fehler; der Wert gehört in einen useEffect.
  8. Server-Komponente in eine Client-Komponente importiert: Sie wird Teil des Client-Bundles; als children übergeben.
  • Next.js: Server and Client Components - wann welche Art, wie man sie kombiniert, children über die Grenze.
  • Next.js: Caching - statische Hülle, "use cache", <Suspense> und Zeitwerte mit Cache Components.
  • Next.js: use cache - die Direktive im Detail, mit Fristen über cacheLife.
  • Next.js: Hydration error - typische Ursachen und Lösungen.
  • Next.js: Next.js CLI - next build, next analyze und die übrigen Befehle.

Du kannst dir diese Einheit kostenlos mit der Vorlesefunktion deines Browsers oder Betriebssystems vorlesen lassen (etwa „Laut vorlesen” in Edge), ohne Konto und ohne Upload; die Codebeispiele und Bäume liest du dabei am Bildschirm mit. Freiwillig kannst du mit Google NotebookLM (Google-Konto nötig) aus diesem öffentlichen Text eine gesprochene Zusammenfassung erzeugen, auch auf Deutsch.

Du kannst jetzt:

  • Server- und Client-Komponenten unterscheiden, nach klaren Regeln zwischen beiden entscheiden und begründen, warum Geheimnisse nie den Client erreichen.
  • mit einem einfachen Experiment nachweisen, wo eine Komponente gerendert wird.
  • entscheiden, ob ein Datenzugriff mit "use cache" zwischengespeichert oder mit <Suspense> bei jeder Anfrage geladen wird, und den Build-Bericht mit ○ und ◐ lesen.
  • Hydration erklären, einen Hydration-Fehler erkennen und beheben.
  • Seiten so in Server- und Client-Anteile zerlegen, dass die Grenze "use client" so tief wie möglich liegt.
  • Aufgabe 08-1a - Mehrseiter mit Next.js, Stufen Wesentlich_vollständig, PlusPlus und MandalorianPlus
  • Server- und Client-Komponenten, Vorrendern von Client-Komponenten, children über die Grenze → Next.js: Server and Client Components
  • Statische Hülle, "use cache", <Suspense>, connection() → Next.js: Caching, use cache, connection; Symbole ○, ◐, ƒ → Next.js: Building your application; force-dynamic mit Cache Components entfallen → Next.js: Route Segment Config
  • Hydration-Fehler und Ursachen → Next.js: Hydration error
  • Build-Fehler bei useState in einer Server-Komponente → eigener Test mit Next.js 16.4.0 (Oktober 2026)
  • Build-Abbrüche und ihr Wortlaut, Build-Bericht mit ○, ◐ und den Fristen 15 Minuten und ein Jahr, Zeiten von /frozen und /live im Produktionsbetrieb, Logs in Terminal und Browser-Konsole, Hydration-Fehler hinter <Suspense> → eigene Tests mit Next.js 16.4.0 und Cache Components in den Beispielprojekten (Oktober 2026)
  • Videos → Web Dev Simplified: WTF Do These Even Mean (p02AIAoImzU); ByteGrad: When & Where to Add “use client” in React / Next.js (Qdkg_mrniLk)
  • Abb. 8.5 (Küche und Gastraum), Abb. 8.8 (statische Hülle, Cache und Streaming), Abb. 8.9 (Hydration), Abb. 8.10 (Client-Grenze) → eigene Darstellung, alle als Mermaid-Diagramm
  • Abb. 8.6 (SSR, assets/08/ssr_prozess.png), Abb. 8.7 (CSR, assets/08/csr_prozess.png) → Lizenz unbekannt (Bestandsbild)
  • Oliver Maurhart (MAO)
  • Michael Gabriel (GAM)

CC-BY-SA

https://qiz.at/rules