Die ‘use client’-Grenze im Next.js App Router richtig zeichnen
Erfahren Sie, was die ‘use client’-Direktive tatsächlich kennzeichnet, warum das Kennzeichnen von Containern die Bundle-Größe erhöht, und wie Leaf-Komponenten sowie Child-Slots die Serverarbeiten auf dem Server bewahren.
Öffnen Sie viele Codebasen von App Router, und Sie werden in fast jedem Komponenten-Code ganz oben 'use client' finden. Diese Gewohnheit entwickelt sich schnell: Sie fügen einen onClick oder ein useState für ein Dropdown hinzu, die Kompilierung wirft einen Fehler aus – doch die Direktive lässt diesen Fehler verschwinden. Wiederholen Sie das ein paar Dutzend Mal, und schließlich landet der gesamte Codebaum im Browser-Bundle, was einer App mit nur einer Seite mit überflüssigen Zusatzfunktionen entspricht. Diese Anleitung erklärt, was die Direktive tatsächlich bewirkt und wie man sie einsetzen sollte, damit serverseitige Aufgaben auf dem Server bleiben und nur wirklich interaktive Teile JavaScript mitliefern.
Was die Direktive tatsächlich kennzeichnet
Der Name deutet auf „Führen Sie dies nur im Browser aus“ hin, doch das ist nicht seine Bedeutung. 'use client' definiert eine Modulgrenze: die Datei, an der das Server-Modulgraph endet und der Client-Bundle beginnt. Client-Komponenten werden weiterhin auf dem Server im HTML vorgerendert; zusätzlich werden sie im Browser aktiviert.
[ Server Component Tree ] (Executes solely on the server; zero KB client JS)
│
├── Server Component A (Fetches directly from DB)
│
▼
── [ 'use client' Boundary ] ──
│
├── Client Component B (Pre-rendered to HTML on server, hydrated on client)
│ │
│ └── Regular Component C (Now forced into the client bundle!)
Die Folge, die Teams in Verwirrung stürzt, ist transitiv. Jedes Modul, das von einer Datei mit der Anweisung 'use client' importiert wird, wird ebenfalls Teil des Client-Bundles – unabhängig davon, ob diese Datei selbst die Anweisung enthält oder nicht. Wenn eine Layout-Struktur für das Hauptdashboard als Client-Modul markiert wird, weil sie ein Dropdown-Menü für Profile enthält, gelangen auch dessen Seitenleiste, Modale, Hilfsfunktionen sowie alle schweren Bibliotheken, die importiert werden, in den Browser. Für eine detailliertere Erklärung dazu, warum serverseitiger Code keine Bundle-Bytes verbraucht, siehe wie React Server Components eine renderung ohne Bundle-Bytes ermöglichen.
Die Falle des interaktiven Containers
Der häufigste Fehler besteht darin, eine ganze Seite in ein Client Component umzuwandeln, nur weil ein kleiner Teil davon interaktiv ist. Das untenstehende Beispiel benötigt den Zustand lediglich für einen Filter-Schalter, doch das gesamte Dashboard wird als Client-Code markiert:
// ❌ BAD: The entire page is pulled into the client bundle
'use client';
import { useState, useEffect } from 'react';
import { db } from '@/lib/db'; // 🚨 Build error or massive security/bundle hazard!
export default function UserDashboard() {
const [isOpen, setIsOpen] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
// You've recreated client-side waterfall fetching
fetch('/api/user-data').then(res => res.json()).then(setData);
}, []);
return (
<div className="p-8">
<button onClick={() => setIsOpen(!isOpen)}>Toggle Filter</button>
{isOpen && <div className="dropdown">...</div>}
<div className="grid">
{/* Render heavy data tables */}
</div>
</div>
);
}
Daraus ergeben sich zwei Probleme. Das Importieren eines Datenbank-Clients in ein Client-Modul führt entweder zum Build-Fehler oder, was noch schlimmer ist, birgt das Risiko, serverseitigen Code und Konfigurationen in den Browser zu bringen. Zudem wird das Laden der Daten in useEffect verlagert, wodurch die Seite zunächst leer gerendert wird und erst nach der Hydratierung Daten anfordert – dadurch entsteht erneut die clientseitige Abfolge von Schritten, die Server Components eigentlich beseitigen sollten. Das Hinzufügen des server-only-Pakets zu Modulen wie @/lib/db verwandelt diesen ersten Fehler in einen expliziten Build-Fehler.
Interaktivität bis zu den untersten Ebenen bringen
Der State gehört zur kleinsten Komponente, die ihn benötigt. Hier wird der Toggle zu einem eigenen Client-Modul, und alles, was er anzeigt, wird als children übergeben:
// components/FilterToggle.tsx
'use client';
import { useState } from 'react';
export function FilterToggle({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button
onClick={() => setIsOpen(!isOpen)}
className="px-3 py-1.5 bg-neutral-100 rounded text-sm"
>
{isOpen ? 'Hide Filters' : 'Show Filters'}
</button>
{isOpen && <div className="mt-2">{children}</div>}
</div>
);
}
Die Seite bleibt eine asynchrone Server-Komponente. Sie fragt direkt die Datenbank ab, rendernt statische Markup-Elemente und embeddet den Toggle nur dort, wo Interaktion stattfindet:
// app/dashboard/page.tsx (Server Component by default)
import { db } from '@/lib/db';
import { FilterToggle } from '@/components/FilterToggle';
import { AnalyticsChart } from '@/components/AnalyticsChart';
export default async function UserDashboard() {
// Direct DB access — zero client-side fetch waterfalls
const metrics = await db.metrics.findMany();
return (
<main className="p-8">
<div className="flex justify-between items-center mb-6">
<h1 className="text-xl font-bold">Performance Analytics</h1>
<FilterToggle>
<p className="text-sm text-neutral-500">Filter options...</p>
</FilterToggle>
</div>
<div className="grid grid-cols-3 gap-4">
{/* AnalyticsChart can also remain an RSC if it doesn't need canvas/DOM APIs */}
<AnalyticsChart data={metrics} />
</div>
</main>
);
}
Der Absatz, der an FilterToggle übergeben wird, wird auf dem Server renderiert, obwohl er sich innerhalb einer Client-Komponente befindet. AnalyticsChart kann weiterhin eine Server-Komponente bleiben, solange es nicht auf browser-eigene APIs wie Canvas angewiesen ist; falls doch, benötigt nur diese Chart-Komponente die entsprechende Direktive.
Serverinhalte innerhalb von Client-Shell-Komponenten komponieren
Die gleiche Technik lässt sich auch auf Layouts anwenden. Man nehme ein CollapsibleShell-Klientenkomponente, die den geöffneten und geschlossenen Zustand verwaltet, ähnlich wie FilterToggle. Das Layout, eine Serverkomponente, erstellt die aufwändige Datenquelle und übergeben sie der Shell als Slot:
// app/layout.tsx
import { CollapsibleShell } from '@/components/CollapsibleShell';
import { ExpensiveServerFeed } from '@/components/ExpensiveServerFeed';
export default function RootLayout() {
return (
<html lang="en">
<body>
<CollapsibleShell>
{/* ExpensiveServerFeed runs on the server, streams HTML, and ships 0kb JS */}
<ExpensiveServerFeed />
</CollapsibleShell>
</body>
</html>
);
}
Weil ExpensiveServerFeed im Serverkontext erstellt wird und als children übergeben wird, wird es vollständig auf dem Server gerendert. React sendet die renderierte Ausgabe über die Grenze im RSC-Payload, und der Code der Komponente gelangt niemals in den Klientenbundel. Der entscheidende Unterschied: Ein Klientenmodul, das eine Komponente importiert, zieht diese in den Bundel ein, während eine Klientenkomponente, die bereits renderierte Elemente als Props erhält, dies nicht tut.
Eine Überprülliste vor dem Hinzufügen der Direktive
- Verwendet dieses Komponente Zustand, Effects, Event-Handler oder Browser-APIs? Falls nicht, lassen Sie es auf dem Server.
- Kann der interaktive Teil in eine kleinere Unterkomponente ausgegliedert werden?
- Kann der auf dem Server gerenderte Inhalt über
childrenoder ein anderes Prop anstelle einer Importierung übergeben werden? - Sind alle Props, die die Grenze überschreiten, serialisierbar – ohne Funktionen oder Klasseninstanzen außer Server Actions?
- Würde das Markieren dieser Datei die Einbeziehung einer schweren Bibliothek wie eines Markdown-Parsers oder von Datentools zur Folge haben, die eigentlich auf dem Server bleiben sollten?
Haupterkenntnisse
- Server Components sind aus gutem Grund die Standardlösung; halten Sie Datenabruf, Parsing-Tools und statische Markup-Daten dort.
- Isoleieren Sie kleine interaktive Einheiten anstelle der Umgebung, in der sie sich befinden.
children sowie andere Slot-Eigenschaften, um Server-Ausgaben in Client-Verhalten einzubetten, ohne sie mitzuschicken.'use client' als bewusste architektonische Grenze und nicht als Mittel, um Build-Fehler zu unterdrücken. Bei richtiger Anwendung bleiben die Bundles klein und der Inhalt wird bereits beim ersten Render bereitgestellt.