React 19: Server Components in der Produktion - Der vollständige Leitfaden

Server Components in React 19 produktionsreif einsetzen. Architektur, Patterns, Streaming, Caching und Optimierungen für hochperformante Anwendungen.

React 19 Server Components Illustration mit vernetzter Server-Client-Architektur

Server Components stellen die bedeutendste Weiterentwicklung in React seit Hooks dar. Mit React 19 hat diese Architektur ihre Reife erreicht und ist produktionsbereit — Komponenten können direkt auf dem Server ausgeführt werden, während die clientseitige Interaktivität erhalten bleibt.

Voraussetzungen

Dieser Leitfaden setzt Erfahrung mit React und dem Next.js App Router voraus. Die Beispiele verwenden Next.js 14+, das React Server Components nativ implementiert.

Die Architektur der Server Components verstehen

Server Components (RSC) führen ein neues Paradigma ein: Einige Komponenten laufen ausschließlich auf dem Server, andere auf dem Client, und beide können im selben Komponentenbaum koexistieren. Diese Trennung optimiert die Performance erheblich, da weniger JavaScript an den Browser gesendet wird.

Die grundlegende Idee basiert darauf, dass viele Komponenten keine Interaktivität benötigen. Eine Komponente, die eine Artikelliste aus einer Datenbank anzeigt, kann beispielsweise vollständig serverseitig ausgeführt werden. Nur interaktive Elemente (Buttons, Formulare, Animationen) erfordern clientseitiges JavaScript.

app/articles/page.tsxtsx
// This component runs only on the server
// No JavaScript is sent to the client for this component
import { getArticles } from '@/lib/articles'
import ArticleCard from './ArticleCard'
import LikeButton from './LikeButton'

// async/await directly in the component
// Only possible with Server Components
export default async function ArticlesPage() {
  // Direct database call (no REST API needed)
  const articles = await getArticles()

  return (
    <main className="container mx-auto py-8">
      <h1 className="text-3xl font-bold mb-6">Recent Articles</h1>

      <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
        {articles.map((article) => (
          // ArticleCard is also a Server Component
          <ArticleCard key={article.id} article={article}>
            {/* LikeButton is a Client Component (interactive) */}
            <LikeButton articleId={article.id} />
          </ArticleCard>
        ))}
      </div>
    </main>
  )
}

Die "use client"-Direktive kennzeichnet explizit Komponenten, die Browser-JavaScript benötigen.

app/articles/LikeButton.tsxtsx
'use client'

// useState and interactive hooks require "use client"
import { useState, useTransition } from 'react'
import { likeArticle } from '@/actions/articles'

interface LikeButtonProps {
  articleId: string
  initialLikes?: number
}

export default function LikeButton({ articleId, initialLikes = 0 }: LikeButtonProps) {
  // Local state for optimistic UI
  const [likes, setLikes] = useState(initialLikes)
  const [isPending, startTransition] = useTransition()

  const handleLike = () => {
    // Immediate optimistic update
    setLikes((prev) => prev + 1)

    // Server Action to persist
    startTransition(async () => {
      await likeArticle(articleId)
    })
  }

  return (
    <button
      onClick={handleLike}
      disabled={isPending}
      className="flex items-center gap-2 px-3 py-1 rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
    >
      <span>❤️</span>
      <span>{likes}</span>
    </button>
  )
}

Diese Architektur reduziert das JavaScript-Bundle erheblich: Nur der Code von LikeButton wird an den Client gesendet, nicht der von ArticlesPage oder ArticleCard.

Kompositionsmuster für Server und Client

Die Komposition zwischen Server und Client Components folgt präzisen Regeln. Eine Server Component kann Client Components importieren und rendern, aber umgekehrt ist das nicht direkt möglich. Um Server-Inhalte an eine Client Component zu übergeben, bietet das children-Pattern die Lösung.

components/InteractiveWrapper.tsxtsx
'use client'

import { useState, ReactNode } from 'react'

interface InteractiveWrapperProps {
  children: ReactNode
  expandable?: boolean
}

// Client Component that wraps server content
export function InteractiveWrapper({ children, expandable = false }: InteractiveWrapperProps) {
  const [isExpanded, setIsExpanded] = useState(!expandable)

  if (!expandable) {
    return <div>{children}</div>
  }

  return (
    <div className="border rounded-lg overflow-hidden">
      <button
        onClick={() => setIsExpanded(!isExpanded)}
        className="w-full p-4 text-left font-medium bg-gray-50 hover:bg-gray-100"
      >
        {isExpanded ? '▼ Collapse' : '▶ Expand'}
      </button>

      {isExpanded && (
        <div className="p-4">
          {/* children can contain Server Components */}
          {children}
        </div>
      )}
    </div>
  )
}
app/dashboard/page.tsxtsx
// Server Component using the client wrapper
import { InteractiveWrapper } from '@/components/InteractiveWrapper'
import { getStats, getRecentActivity } from '@/lib/dashboard'

export default async function DashboardPage() {
  // Parallel server-side requests
  const [stats, activity] = await Promise.all([
    getStats(),
    getRecentActivity()
  ])

  return (
    <div className="space-y-6">
      {/* Stats in an expandable wrapper */}
      <InteractiveWrapper expandable>
        {/* This content is rendered server-side then passed to client */}
        <div className="grid grid-cols-3 gap-4">
          <StatCard title="Users" value={stats.users} />
          <StatCard title="Revenue" value={stats.revenue} />
          <StatCard title="Orders" value={stats.orders} />
        </div>
      </InteractiveWrapper>

      {/* Recent activity */}
      <InteractiveWrapper>
        <ActivityList activities={activity} />
      </InteractiveWrapper>
    </div>
  )
}

Dieses Pattern ermöglicht es, clientseitige Interaktivität mit serverseitig gerenderten Daten zu kombinieren, ohne Logik zu duplizieren.

Datenabruf und Caching

Erweiterte fetch-API durch React

React 19 erweitert die native fetch-API automatisch um Deduplizierung und Caching. Identische Anfragen innerhalb desselben Rendervorgangs werden nur einmal ausgeführt.

Der Datenabruf in Server Components erfolgt direkt mit async/await. React übernimmt automatisch die Deduplizierung identischer Anfragen.

lib/api.tstsx
// Centralized request configuration with caching
const API_BASE = process.env.API_URL

// Request with time-based revalidation
export async function getProducts() {
  const response = await fetch(`${API_BASE}/products`, {
    // Revalidate every hour
    next: { revalidate: 3600 }
  })

  if (!response.ok) {
    throw new Error('Failed to fetch products')
  }

  return response.json()
}

// Request without cache (real-time data)
export async function getCurrentUser() {
  const response = await fetch(`${API_BASE}/me`, {
    // No cache, always fresh
    cache: 'no-store'
  })

  if (!response.ok) {
    return null
  }

  return response.json()
}

// Request with tag for targeted invalidation
export async function getProduct(id: string) {
  const response = await fetch(`${API_BASE}/products/${id}`, {
    next: {
      tags: [`product-${id}`],
      revalidate: 3600
    }
  })

  if (!response.ok) {
    throw new Error('Product not found')
  }

  return response.json()
}

Für den direkten Datenbankzugriff (Prisma, Drizzle) bietet React Cache mit unstable_cache die gleichen Möglichkeiten.

lib/db-queries.tstsx
import { unstable_cache } from 'next/cache'
import { prisma } from '@/lib/prisma'

// Cache categories (rarely modified)
export const getCategories = unstable_cache(
  async () => {
    return prisma.category.findMany({
      orderBy: { name: 'asc' }
    })
  },
  ['categories'], // Cache key
  {
    revalidate: 86400, // 24 hours
    tags: ['categories']
  }
)

// Cache products by category
export const getProductsByCategory = unstable_cache(
  async (categoryId: string) => {
    return prisma.product.findMany({
      where: { categoryId },
      include: { images: true },
      orderBy: { createdAt: 'desc' }
    })
  },
  ['products-by-category'],
  {
    revalidate: 3600,
    tags: ['products']
  }
)

// Cache invalidation after mutation
export async function createProduct(data: ProductInput) {
  const product = await prisma.product.create({ data })

  // Invalidate related caches
  revalidateTag('products')

  return product
}

Bereit für deine React / Next.js-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Streaming und Suspense für optimale UX

Streaming ermöglicht das schrittweise Senden von HTML an den Browser, wobei verfügbare Teile sofort angezeigt werden, während andere noch laden. In Kombination mit Suspense verbessert dieser Mechanismus die Time to First Byte (TTFB) und die wahrgenommene Benutzererfahrung erheblich.

app/product/[id]/page.tsxtsx
import { Suspense } from 'react'
import { getProduct } from '@/lib/products'
import ProductDetails from './ProductDetails'
import ProductReviews from './ProductReviews'
import RecommendedProducts from './RecommendedProducts'
import { Skeleton } from '@/components/ui/Skeleton'

interface ProductPageProps {
  params: { id: string }
}

export default async function ProductPage({ params }: ProductPageProps) {
  // This request blocks initial render
  const product = await getProduct(params.id)

  return (
    <div className="container mx-auto py-8">
      {/* Immediate render with product data */}
      <ProductDetails product={product} />

      {/* Reviews load via streaming */}
      <section className="mt-12">
        <h2 className="text-2xl font-bold mb-6">Customer Reviews</h2>
        <Suspense fallback={<ReviewsSkeleton />}>
          {/* This async component will be streamed */}
          <ProductReviews productId={params.id} />
        </Suspense>
      </section>

      {/* Recommendations too */}
      <section className="mt-12">
        <h2 className="text-2xl font-bold mb-6">Similar Products</h2>
        <Suspense fallback={<ProductGridSkeleton count={4} />}>
          <RecommendedProducts productId={params.id} />
        </Suspense>
      </section>
    </div>
  )
}

// Skeleton for reviews
function ReviewsSkeleton() {
  return (
    <div className="space-y-4">
      {[1, 2, 3].map((i) => (
        <div key={i} className="border rounded-lg p-4">
          <Skeleton className="h-4 w-32 mb-2" />
          <Skeleton className="h-4 w-full" />
          <Skeleton className="h-4 w-3/4" />
        </div>
      ))}
    </div>
  )
}
app/product/[id]/ProductReviews.tsxtsx
// Async Server Component that will be streamed
import { getProductReviews } from '@/lib/reviews'

interface ProductReviewsProps {
  productId: string
}

export default async function ProductReviews({ productId }: ProductReviewsProps) {
  // This request may take time
  // Component will be streamed when complete
  const reviews = await getProductReviews(productId)

  if (reviews.length === 0) {
    return (
      <p className="text-gray-500 italic">
        No reviews yet. Be the first to share your thoughts!
      </p>
    )
  }

  return (
    <div className="space-y-4">
      {reviews.map((review) => (
        <article key={review.id} className="border rounded-lg p-4">
          <div className="flex items-center gap-2 mb-2">
            <span className="font-medium">{review.author}</span>
            <span className="text-yellow-500">
              {'★'.repeat(review.rating)}{'☆'.repeat(5 - review.rating)}
            </span>
          </div>
          <p className="text-gray-700">{review.content}</p>
          <time className="text-sm text-gray-500">
            {new Date(review.createdAt).toLocaleDateString('en-US')}
          </time>
        </article>
      ))}
    </div>
  )
}

Der Browser empfängt zuerst das HTML-Grundgerüst mit Skeletons, anschließend wird der tatsächliche Inhalt schrittweise per Streaming eingefügt.

Server Actions für Mutationen

Server Actions ermöglichen die Ausführung von Server-Code aus Client Components heraus, ohne API-Routen erstellen zu müssen. Dieser Ansatz vereinfacht die Handhabung von Mutationen erheblich.

actions/cart.tstsx
'use server'

import { revalidatePath } from 'next/cache'
import { cookies } from 'next/headers'
import { prisma } from '@/lib/prisma'
import { getCurrentUser } from '@/lib/auth'

// Action to add to cart
export async function addToCart(productId: string, quantity: number = 1) {
  const user = await getCurrentUser()

  if (!user) {
    // Return structured error
    return { error: 'Login required', code: 'UNAUTHORIZED' }
  }

  try {
    // Check stock
    const product = await prisma.product.findUnique({
      where: { id: productId }
    })

    if (!product || product.stock < quantity) {
      return { error: 'Insufficient stock', code: 'OUT_OF_STOCK' }
    }

    // Add or update cart item
    await prisma.cartItem.upsert({
      where: {
        cartId_productId: {
          cartId: user.cartId,
          productId
        }
      },
      update: {
        quantity: { increment: quantity }
      },
      create: {
        cartId: user.cartId,
        productId,
        quantity
      }
    })

    // Invalidate cart page cache
    revalidatePath('/cart')

    return { success: true, message: 'Product added to cart' }
  } catch (error) {
    console.error('Add to cart error:', error)
    return { error: 'An error occurred', code: 'SERVER_ERROR' }
  }
}

// Action to remove from cart
export async function removeFromCart(itemId: string) {
  const user = await getCurrentUser()

  if (!user) {
    return { error: 'Login required' }
  }

  await prisma.cartItem.delete({
    where: { id: itemId, cart: { userId: user.id } }
  })

  revalidatePath('/cart')

  return { success: true }
}
components/AddToCartButton.tsxtsx
'use client'

import { useTransition } from 'react'
import { addToCart } from '@/actions/cart'
import { toast } from '@/components/ui/toast'

interface AddToCartButtonProps {
  productId: string
}

export function AddToCartButton({ productId }: AddToCartButtonProps) {
  const [isPending, startTransition] = useTransition()

  const handleClick = () => {
    startTransition(async () => {
      const result = await addToCart(productId)

      if (result.error) {
        toast.error(result.error)
        return
      }

      toast.success(result.message)
    })
  }

  return (
    <button
      onClick={handleClick}
      disabled={isPending}
      className="w-full py-3 px-6 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
    >
      {isPending ? (
        <span className="flex items-center justify-center gap-2">
          <span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
          Adding...
        </span>
      ) : (
        'Add to Cart'
      )}
    </button>
  )
}
Serverseitige Validierung

Daten sollten in Server Actions immer validiert werden. Clientseitige Validierungen lassen sich umgehen. Bibliotheken wie Zod bieten eine robuste Validierung.

Fehlerbehandlung und Error Boundaries

React 19 verbessert die Fehlerbehandlung bei Server Components. Error Boundaries funktionieren genauso wie bei Client Components.

app/products/error.tsxtsx
'use client'

// Error Boundary for /products segment
interface ErrorProps {
  error: Error & { digest?: string }
  reset: () => void
}

export default function ProductsError({ error, reset }: ErrorProps) {
  return (
    <div className="flex flex-col items-center justify-center min-h-[400px] text-center">
      <div className="p-6 bg-red-50 rounded-lg max-w-md">
        <h2 className="text-xl font-bold text-red-800 mb-2">
          Loading Error
        </h2>
        <p className="text-red-600 mb-4">
          Unable to load products. Please try again.
        </p>
        {/* Display digest for debugging */}
        {error.digest && (
          <p className="text-sm text-gray-500 mb-4">
            Reference: {error.digest}
          </p>
        )}
        <button
          onClick={reset}
          className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
        >
          Try Again
        </button>
      </div>
    </div>
  )
}
app/products/loading.tsxtsx
// Loading UI during initial load
export default function ProductsLoading() {
  return (
    <div className="container mx-auto py-8">
      <div className="h-8 w-48 bg-gray-200 rounded animate-pulse mb-6" />
      <div className="grid grid-cols-3 gap-6">
        {[1, 2, 3, 4, 5, 6].map((i) => (
          <div key={i} className="border rounded-lg p-4">
            <div className="aspect-square bg-gray-200 rounded animate-pulse mb-4" />
            <div className="h-4 bg-gray-200 rounded animate-pulse mb-2" />
            <div className="h-4 w-2/3 bg-gray-200 rounded animate-pulse" />
          </div>
        ))}
      </div>
    </div>
  )
}

Für eine feinere Fehlerbehandlung in einzelnen Komponenten kann die ErrorBoundary-Komponente direkt eingesetzt werden.

components/ErrorBoundary.tsxtsx
'use client'

import { Component, ReactNode } from 'react'

interface Props {
  children: ReactNode
  fallback?: ReactNode
}

interface State {
  hasError: boolean
  error?: Error
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { hasError: false }
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Log error to monitoring service
    console.error('ErrorBoundary caught:', error, errorInfo)
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="p-4 bg-red-50 rounded-lg">
          <p className="text-red-600">An error occurred</p>
        </div>
      )
    }

    return this.props.children
  }
}

Performance-Optimierungen für die Produktion

Mehrere Techniken ermöglichen die Optimierung der Server Components Performance in der Produktion.

app/layout.tsxtsx
import { Suspense } from 'react'
import { headers } from 'next/headers'

// Preload critical data
export const dynamic = 'force-dynamic'

export default async function RootLayout({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        {/* Header streamed independently */}
        <Suspense fallback={<HeaderSkeleton />}>
          <Header />
        </Suspense>

        <main>{children}</main>

        {/* Footer can be static */}
        <Footer />
      </body>
    </html>
  )
}
lib/prefetch.tstsx
// Data prefetching for links
import { preload } from 'react-dom'

export function prefetchProduct(productId: string) {
  // Prefetch images
  preload(`/api/products/${productId}/image`, { as: 'image' })
}

// Usage in a component
// <Link href={`/products/${id}`} onMouseEnter={() => prefetchProduct(id)}>
next.config.jstsx
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Build optimizations
  experimental: {
    // Enable Partial Prerendering (PPR)
    ppr: true,
    // Optimize package imports
    optimizePackageImports: ['lucide-react', '@radix-ui/react-icons']
  },

  // Image configuration
  images: {
    formats: ['image/avif', 'image/webp'],
    remotePatterns: [
      { hostname: 'cdn.example.com' }
    ]
  }
}

module.exports = nextConfig

Bereit für deine React / Next.js-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Fazit

React 19 Server Components verändern grundlegend, wie React-Anwendungen gebaut werden. Die wichtigsten Erkenntnisse:

  • Server/Client-Trennung: "use client" nur für interaktive Komponenten verwenden
  • Direkter Datenabruf: async/await in Komponenten, ohne useEffect oder API-Routen
  • Streaming mit Suspense: Schrittweise Darstellung für eine bessere wahrgenommene UX
  • Server Actions: Vereinfachte Mutationen ohne eigene API-Endpunkte
  • Intelligentes Caching: revalidate und tags zur Performance-Optimierung
  • Flexible Komposition: children-Pattern zum Kombinieren von Server und Client Components

Diese Architektur ermöglicht performantere Anwendungen mit weniger clientseitigem JavaScript und vereinfacht gleichzeitig den Code erheblich. Der Umstieg auf Server Components ist eine Investition, die sich schnell in Sachen Performance und Wartbarkeit auszahlt.

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tags

#react 19
#server components
#rsc
#performance
#next.js

Teilen

Verwandte Artikel