React Server Components (RSC) sind die groesste architektonische Veraenderung seit Hooks.
Was sind RSC?
Komponenten, die auf dem Server gerendert werden.
Drei Arten von Komponenten im RSC-Modell.
Server vs Client
| Aspekt | Server Component | Client Component |
|---|---|---|
| Standard | Ja | Nein |
| Direktive | Keine | "use client" |
| Client-JS | Kein (Zero JS) | Ja |
| useState/useEffect | Nicht verfuegbar | Verfuegbar |
| Event-Handler | Nicht verfuegbar | Verfuegbar |
| DB-Zugriff | Ja | Nein |
| Dateisystem | Ja | Nein |
| Server-Umgebungsvariablen | Ja | Nein |
| Async-Komponente | Ja | Nein |
| Bundle-Groesse | Null | Erhoehung |
Entscheidungsbaum
Should this component be a Server or Client Component?
Does it need useState, useEffect, or useRef?
âââ YES â Client Component ("use client")
âââ NO â
Does it need event handlers (onClick, onChange, onSubmit)?
âââ YES â Client Component ("use client")
âââ NO â
Does it use browser-only APIs (window, localStorage, navigator)?
âââ YES â Client Component ("use client")
âââ NO â
Does it use third-party libraries that require hooks or browser APIs?
âââ YES â Client Component ("use client")
âââ NO â
Does it fetch data or access databases/file system?
âââ YES â Server Component (default) â BEST CHOICE
âââ NO â Server Component (default) â still the defaultRSC-Patterns
Pattern 1: Server-Datenabruf
Direkter Datenabruf mit async/await.
// app/users/page.tsx â Server Component (default, no directive needed)
import { db } from '@/lib/database';
// This component is async â only possible in Server Components
export default async function UsersPage() {
// Direct database query â this code NEVER reaches the client
const users = await db.user.findMany({
orderBy: { createdAt: 'desc' },
take: 50,
});
return (
<div>
<h1>Users ({users.length})</h1>
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} â {user.email}
</li>
))}
</ul>
</div>
);
}
// Benefits:
// - Zero JavaScript sent to client for this component
// - No loading states needed (data is available before render)
// - Database credentials never exposed to the browser
// - No useEffect + fetch + useState dancePattern 2: Komposition
Server kann Client rendern.
// components/LikeButton.tsx â Client Component
'use client';
import { useState } from 'react';
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
const [count, setCount] = useState(0);
return (
<button onClick={() => {
setLiked(!liked);
setCount(c => liked ? c - 1 : c + 1);
}}>
{liked ? 'Unlike' : 'Like'} ({count})
</button>
);
}
// app/posts/[id]/page.tsx â Server Component
import { db } from '@/lib/database';
import { LikeButton } from '@/components/LikeButton';
export default async function PostPage({ params }: { params: { id: string } }) {
// Server: fetch data
const post = await db.post.findUnique({ where: { id: params.id } });
return (
<article>
{/* Server-rendered content (zero JS) */}
<h1>{post?.title}</h1>
<div>{post?.content}</div>
{/* Client Component island (only this ships JS) */}
<LikeButton postId={params.id} />
</article>
);
}
// Pattern: Server Component as children of Client Component
// components/Sidebar.tsx
'use client';
export function Sidebar({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(true);
return (
<aside style={{ display: open ? 'block' : 'none' }}>
<button onClick={() => setOpen(!open)}>Toggle</button>
{children} {/* Server Component content passed as children */}
</aside>
);
}Pattern 3: Server Actions
Server-Funktionen direkt aufrufen.
// app/actions.ts â Server Action
'use server';
import { db } from '@/lib/database';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.post.create({
data: { title, content, authorId: 'user-1' },
});
revalidatePath('/posts');
}
export async function deletePost(postId: string) {
await db.post.delete({ where: { id: postId } });
revalidatePath('/posts');
}
// components/CreatePostForm.tsx â Client Component using Server Action
'use client';
import { createPost } from '@/app/actions';
import { useActionState } from 'react';
export function CreatePostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Post title" required />
<textarea name="content" placeholder="Write your post..." required />
<button type="submit">Create Post</button>
</form>
);
}
// Works without API routes!
// The form submits directly to the server action
// Progressive enhancement: works without JavaScriptPattern 4: Streaming
Langsame Daten blockieren nicht die Seite.
// app/dashboard/page.tsx â Streaming with Suspense
import { Suspense } from 'react';
// Slow component â fetches from external API
async function AnalyticsChart() {
const data = await fetch('https://api.analytics.com/data', {
next: { revalidate: 60 },
});
const analytics = await data.json();
return <div>Chart: {analytics.visitors} visitors</div>;
}
// Fast component â fetches from local DB
async function RecentPosts() {
const posts = await db.post.findMany({ take: 5 });
return (
<ul>
{posts.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* This renders immediately */}
<Suspense fallback={<div>Loading posts...</div>}>
<RecentPosts />
</Suspense>
{/* This streams in when ready (doesn't block the page) */}
<Suspense fallback={<div>Loading analytics...</div>}>
<AnalyticsChart />
</Suspense>
</div>
);
}
// Result: User sees "Dashboard" + posts immediately
// Analytics chart streams in when the API responds
// No client-side loading spinners neededDatenabruf-Strategien
Sequentiell
Bei abhaengigen Anfragen.
// Sequential: second request depends on first
async function UserProfile({ userId }: { userId: string }) {
const user = await getUser(userId); // 1st request
const posts = await getUserPosts(user.name); // 2nd (depends on user.name)
return <div>{user.name}: {posts.length} posts</div>;
}Parallel
Promise.all fuer unabhaengige Anfragen.
// Parallel: independent requests â use Promise.all
async function Dashboard() {
// Start all requests simultaneously
const [users, posts, analytics] = await Promise.all([
getUsers(),
getPosts(),
getAnalytics(),
]);
return (
<div>
<UserList users={users} />
<PostList posts={posts} />
<AnalyticsPanel data={analytics} />
</div>
);
}Caching und Revalidierung
Next.js erweitert fetch mit Cache-Kontrolle.
// Next.js fetch caching strategies
// 1. Static (cached indefinitely until revalidated)
const data = await fetch('https://api.example.com/data');
// Equivalent to: { cache: 'force-cache' }
// 2. Revalidate every 60 seconds (ISR)
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 },
});
// 3. Dynamic (no caching â fresh on every request)
const data = await fetch('https://api.example.com/data', {
cache: 'no-store',
});
// 4. Tag-based revalidation
const data = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
});
// In a Server Action:
import { revalidateTag } from 'next/cache';
revalidateTag('posts'); // Purge all fetches tagged 'posts'Performance-Vorteile
RSC bietet mehrere Leistungsvorteile.
RSC Performance Benefits:
1. Zero Client JavaScript for Server Components
- Only Client Components ship JS to the browser
- Large libraries used on server (markdown, syntax highlighting) add zero bundle
2. Streaming SSR
- HTML streams to browser as components resolve
- First Contentful Paint (FCP) is faster
- Time to Interactive (TTI) is faster (less JS to hydrate)
3. Automatic Code Splitting
- Each Client Component is automatically code-split
- No manual dynamic imports needed
4. Reduced Data Transfer
- Server Components fetch data on the server (same network)
- No client-side fetch waterfalls
- No duplicate data in both HTML and JS payload
5. Smaller Hydration Payload
- Only Client Components need hydration
- Server Component output is static â no hydration needed
Typical Results:
Before RSC: 450KB JS bundle, 2.5s TTI
After RSC: 180KB JS bundle, 1.1s TTI (60% JS reduction)Haeufige Fehler
// Mistake 1: Adding "use client" unnecessarily
// BAD: This doesn't need client-side JavaScript
'use client'; // REMOVE THIS
export function UserCard({ name, email }: { name: string; email: string }) {
return <div>{name} ({email})</div>; // No hooks, no events = Server Component
}
// Mistake 2: Importing Server Component in Client Component
// BAD: This won't work
'use client';
import { ServerComponent } from './ServerComponent'; // ERROR
// FIX: Pass as children instead
export function ClientWrapper({ children }: { children: React.ReactNode }) {
return <div>{children}</div>;
}
// Mistake 3: Using "use client" too high in the tree
// BAD: Making a layout a Client Component
'use client'; // This makes EVERYTHING below it client-side
export function Layout({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState('dark');
return <div className={theme}>{children}</div>;
}
// FIX: Extract the interactive part into a small Client Component
// components/ThemeToggle.tsx
'use client';
export function ThemeToggle() {
const [theme, setTheme] = useState('dark');
return <button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>{theme}</button>;
}
// layout.tsx â stays as Server Component
export function Layout({ children }: { children: React.ReactNode }) {
return <div><ThemeToggle />{children}</div>;
}Haeufige Fragen
RSC ohne Next.js?
Technisch ja. Praktisch ist Next.js das Hauptframework.
Ersetzt RSC SSR?
Nein, ergaenzt es.
Wann "use client"?
Bei Hooks oder Browser-APIs.
Context in Server Components?
Nicht verfuegbar.
SEO-Auswirkung?
Positiv. HTML auf dem Server generiert.
Verwandte Tools und Anleitungen
- JSON Formatter - Format API responses and data
- JSON to TypeScript - Generate types from API responses
- HTML to JSX - Convert HTML to React JSX
- Next.js App Router Guide
- React Hooks Guide
- TypeScript Generics Guide
- Web Performance Optimization