8.2 Next.js: Server- und Client-Komponenten
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.
Begriffe vorab
Abschnitt betitelt „Begriffe vorab“- 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.
Server- und Client-Komponenten
Abschnitt betitelt „Server- und Client-Komponenten“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):
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 fileimport { 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:
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:
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:
| Bedarf | Server | Client |
|---|---|---|
| Daten laden (Datei, Datenbank, interne API) | ja | über HTTP |
| Geheimnisse (Schlüssel, Zugangsdaten) | ja | niemals |
| Hooks, Events, Interaktivität | nein | ja |
Browser-APIs (localStorage, …) | nein | ja |
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):
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".
Statische Hülle, Cache und Streaming
Abschnitt betitelt „Statische Hülle, Cache und Streaming“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.tsxaus Einheit 8.1 ist genau so eineSuspense-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 shellexport default async function FrozenPage() { "use cache"; return <p>Rendered at {new Date().toISOString()}</p>;}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.
Hydration: vom HTML zur bedienbaren Seite
Abschnitt betitelt „Hydration: vom HTML zur bedienbaren Seite“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.
Wie weit "use client" reicht
Abschnitt betitelt „Wie weit "use client" reicht“"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
childrendurch 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:
"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.
Stolperfallen
Abschnitt betitelt „Stolperfallen“"use client"im Wurzel-Layout: Die ganze Anwendung wird zur Client-Anwendung.- Hook in einer Server-Komponente:
useStateohne"use client"bricht den Build ab (“You’re importing a module that depends onuseStateinto a React Server Component module”); die Direktive gehört in den kleinsten interaktiven Baustein. - Geheimnis als Prop an eine Client-Komponente: Alles, was die Grenze überquert, steht im Browser.
- Datenzugriff ohne Entscheidung: Datei,
fetch, Datenbank,paramsodernew Date()ohne"use cache"oder<Suspense>; der Build bricht ab. "use cache"für persönliche Daten: Was je Besucher verschieden ist (Name, Warenkorb), gehört hinter<Suspense>, sonst sehen alle dasselbe.export const dynamicaus alten Anleitungen: Mit Cache Components entfallen; der Build meldet, dass die Angabe nicht kompatibel ist.- Zeit oder Zufall im ersten Rendern: Hydration-Fehler; der Wert gehört in einen
useEffect. - Server-Komponente in eine Client-Komponente importiert: Sie wird Teil des Client-Bundles; als
childrenübergeben.
Zusatzmaterialien
Abschnitt betitelt „Zusatzmaterialien“- 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 analyzeund die übrigen Befehle.
Zum Anhören
Abschnitt betitelt „Zum Anhören“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.
Lernergebnisse
Abschnitt betitelt „Lernergebnisse“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.
Passende Übungen
Abschnitt betitelt „Passende Übungen“- Aufgabe 08-1a - Mehrseiter mit Next.js, Stufen Wesentlich_vollständig, PlusPlus und MandalorianPlus
Quellen
Abschnitt betitelt „Quellen“- 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-dynamicmit Cache Components entfallen → Next.js: Route Segment Config - Hydration-Fehler und Ursachen → Next.js: Hydration error
- Build-Fehler bei
useStatein 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/frozenund/liveim 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)
Autoren
Abschnitt betitelt „Autoren“- Oliver Maurhart (MAO)
- Michael Gabriel (GAM)
CC-BY-SA