Startseite / Artikel / Wo soll die Server-Client-Grenze in einer Next.js App Router-Seite gezogen werden?

Wo soll die Server-Client-Grenze in einer Next.js App Router-Seite gezogen werden?

Ein praktisches mentales Modell für React Server Components: Was wo ausgeführt wird, wie eine Blogartikel-Seite in Server- und Client-Bereiche aufgeteilt wird, sowie die einzuhaltenden Importregeln.

1321 Wörter

React Server Components wirken oft rätselhaft, selbst nachdem man die Dokumentation gelesen hat – hauptsächlich deshalb, weil sie von einem Annahmen aufgeben verlangen, die jeder React-Entwickler seit Jahren hatte: dass Komponenten im Browser ausgeführt werden. Sobald diese Annahme fällt, folgt der Rest recht natürlich. Dieser Artikel entwickelt ein mentales Modell anhand einer einzelnen Blog-Post-Seite und zeigt, welche Teile auf dem Server liegen sollten, welche den Client benötigen, sowie die wenigen Regeln, die die Grenze zwischen ihnen klar halten.

Für einen tieferen Einblick in den Rendering-Prozess selbst siehe die Architektur hinter dem zero-Bundle-Rendering mit Server Components.

Von „Alles wird hydratisiert“ zu „Nur das, was nötig ist“

Vor Server Components wurde jede React-Komponente letztendlich im Browser ausgeführt. Selbst bei serverseitiger Renderung wurde die JavaScript-Codebasis für den gesamten Baum an den Client gesendet und dort initialisiert, damit die Anwendung interaktiv werden konnte. Das ist verschwenderisch für Komponenten, die lediglich Daten abrufen und diese als Props weitergeben – ihr Code wird heruntergeladen und ausgeführt, ohne jemals auf den Benutzer zu reagieren.

Server Components ändern das, indem sie ausschließlich auf dem Server ausgeführt werden. Ihr Code wird niemals an den Browser gesendet und auch nicht initialisiert, wodurch kein JavaScript in den Client-Bundle aufgenommen wird. Was beim Browser ankommt, ist die bereits renderierte Ausgabe, die React zusammen mit dem HTML in einen kompakten Datensatz serialisiert.

Die einfachste Art, sich diese beiden Arten zu merken:

  • Client Component: wird im Browser ausgeführt (nach vorheriger Vorenderung auf dem Server), kann Zustand speichern und auf Ereignisse reagieren.
  • Serverkomponente: läuft auf dem Server, kann Daten direkt aus Datenbanken oder Dateien lesen und sendet nur das gerenderte Ausgabeformat zurück.
  • Warum die Unterscheidung vorteilhaft ist

    Nehmen wir eine typische Blog-Beitragseite. Der Titel und der Text stammen aus einer Datenbank, sehen für jeden Besucher gleich aus und berücksichtigen Klicks nicht. Traditionell gelangt der Code, der sie darstellt, zusammen mit allen Markdown- oder Formatierungsbibliotheken dennoch zum Browser. Mit Serverkomponenten bleibt all das auf dem Server. Dadurch wird die Dateigröße verringert und die Seiten laden in der Regel schneller, insbesondere auf langsameren Geräten.

    Teams, die zum App Router wechseln, der auf Serverkomponenten basiert, berichten in einigen Fällen von besseren Core Web Vitals-Werten, insbesondere beim LCP. Betrachten Sie dies als kontextabhängig: Der Vorteil hängt davon ab, wie viel Client-JavaScript Ihre Seiten weglassen können – messen Sie daher Ihre eigenen Routen.

    Eine Blog-Beitragseite, in zwei Teile geteilt

    In einem Next.js App Router-Projekt ist eine Seite-Datei standardmäßig ein Server Component, es sei denn, es steht etwas anderes darauf. Die folgende Seite ist eine async-Funktion, die direkt von der Datenschicht auf den Post wartet, den Fall „nicht gefunden“ handhabt, den statischen Inhalt rendernt und anschließend ein einzelnes interaktives Kindelement für Kommentare einfügt. Sie ist nach der Next.js 14 API geschrieben:

    // app/posts/[slug]/page.tsx
    // This is a Server Component by default — no "use client" needed
    
    import { getPostBySlug } from "@/lib/db"
    import { PostContent } from "@/components/PostContent"
    import { CommentSection } from "@/components/CommentSection"
    
    type Props = {
      params: { slug: string }
    }
    
    export default async function PostPage({ params }: Props) {
      // Direct DB call — no useEffect, no API route, no loading state
      const post = await getPostBySlug(params.slug)
    
      if (!post) {
        return <div>Post not found.</div>
      }
    
      return (
        <article className="max-w-2xl mx-auto py-12 px-4">
          <h1 className="text-3xl font-bold mb-4">{post.title}</h1>
          <PostContent content={post.body} />
    
          {/* This one needs interactivity — so it's a Client Component */}
          <CommentSection postId={post.id} />
        </article>
      )
    }
    

    Beachten Sie, was fehlt: kein useState, kein useEffect, keine API-Route-Umhüllung und keine Verwaltung eines Ladezustands. Die Daten werden genauso wie in jeder anderen async-Funktion abgewartet, was der Kern dieses Modells ist.

    Zwei praktische Hinweise. Ab Next.js 15 wird params als Promise übergeben, sodass die Seite zuerst await params ausführen muss, bevor sie slug liest; überprüfen Sie daher die von Ihnen verwendete Version. Bei einem echten „Not Found“-Fall gibt das Aufrufen von notFound() aus next/navigation einen korrekten 404-Status zurück, anstatt eine normale Seite mit einer Fehlermeldung.

    Das Kommentarfeld ist anders. Es speichert den Entwurftext im Zustand und reagiert auf Eingaben sowie Klicks, weshalb es im Browser ausgeführt werden muss. Die Direktive "use client" am Anfang der Datei kennzeichnet es als Client-Komponente:

    // components/CommentSection.tsx
    "use client" // opts into browser rendering
    
    import { useState } from "react"
    
    type Props = {
      postId: string
    }
    
    export function CommentSection({ postId }: Props) {
      const [comment, setComment] = useState("")
    
      const handleSubmit = async () => {
        await fetch("/api/comments", {
          method: "POST",
          body: JSON.stringify({ postId, comment }),
        })
        setComment("")
      }
    
      return (
        <div className="mt-8">
          <textarea
            value={comment}
            onChange={(e) => setComment(e.target.value)}
            placeholder="Leave a comment..."
            className="w-full border rounded p-2 text-sm"
          />
          <button
            onClick={handleSubmit}
            className="mt-2 bg-blue-600 text-white px-4 py-2 rounded text-sm"
          >
            Post Comment
          </button>
        </div>
      )
    }
    

    Alles, was interaktiv ist, befindet sich hier: der lokale Zustand für das Textfeld, ein Handler für Änderungen sowie ein Submit-Handler, der eine Anfrage an einen API-Pfad sendet und anschließend das Feld leert. Bei der tatsächlichen Implementierung sollte man im Anfrageheader Content-Type: application/json angeben und eine Server Action als Alternative zu einem separaten API-Pfad in Betracht ziehen.

    Durch diese Trennung wird die Struktur leicht verständlich: Der Server verwaltet die Daten, der Client kümmert sich um die Interaktion.

    Regeln zur Aufrechterhaltung einer klaren Trennung

    • Im App Router sind Komponenten standardmäßig Server Components.
    • Fügen Sie "use client" nur dort hinzu, wo Sie Zustand, Effekte, Event-Handler oder Browser-APIs wie window und localStorage benötigen.
    • Server Components können Client Components importieren und rendern, wie es die Seite mit CommentSection tut.
  • Client-Komponenten können keine Server-Komponenten importieren, aber sie können diese als children oder andere Props erhalten, was es ermöglicht, serverseitig gerenderten Inhalt innerhalb einer interaktiven Schale zu verschieben.
  • Props, die vom Server an den Client übergeben werden, müssen serialisierbar sein: Einfache Daten funktionieren, Funktionen und Klasseninstanzen hingegen nicht.
  • Das Styling bleibt unberührt. Tailwind-Klassen und CSS funktionieren in beiden Komponententypen genauso.
  • "use client" markiert außerdem eine Grenze und nicht nur eine einzelne Datei: Alles, was von dieser Datei importiert wird, gehört zum Client-Bundle. Die Anweisung so tief wie möglich im Baum, auf den kleinen interaktiven Knoten, zu platzieren, hält den Rest auf dem Server.

    Zusammenfassung

    Das mentale Modell funktioniert sofort – „Wo wird das ausgeführt?“ ist die erste Frage, die man sich zu einer Komponente stellt, und die Standardantwort des App Routers ist der Server.

    • Server Components verlagern die Darstellung und den Datenzugriff auf den Server und liefern kein Component-JavaScript mit.
    • Der Datenzugriff erfolgt innerhalb des Components einfach über async/await.
    • Betrachten Sie "use client" als Option für interaktive Komponenten, nicht als Standard.
    • Ein praktischer erster Schritt: Wählen Sie eine Datenerfassungskomponente in einem bestehenden Projekt aus, prüfen Sie, ob sie tatsächlich den Browser benötigt, und konvertieren Sie sie ggf.

    Sobald dieses Modell etabliert ist, werden Layouts, Suspense-Streaming, Server Actions sowie parallele Routen viel leichter zu erlernen, da alles auf derselben serverbasierten Grundlage aufbaut.

    Verwandte Literatur

  • Migrieren von next/router: Params, Shallow URLs und 404-Fehler im App Router — Erfahren Sie, wie Params, useParams, Updates von Shallow URLs, programmatische Navigation sowie echte 404-Seiten im Next.js App Router funktionieren, nachdem next/router nicht mehr verwendet wird.
  • Technisches SEO im Next.js App Router: Metadata, Sitemaps und JSON-LD — Lernen Sie, wie gemeinsame Metadata-Hilfsfunktionen, Standard-Einstellungen für das Root-Layout, robots.ts, ein dynamischer Sitemap, korrektes JSON-LD sowie Seitenaudits einer Next.js-Anwendung eine solide SEO-Base bieten.
  • Warum WebSockets in Next.js API-Routen hängen und wie ein kundenspezifischer Server das behebt — Erfahren Sie, warum ein WS-Server in einer pages/api-Route niemals die Verbindungsherstellung abschließt, und wie Sie das Upgrade-Event mit einem eigenen Next.js-Server selbst handhaben können.