React-Performance: Patterns, die wirklich etwas bewirken
Praktische Performance-Techniken für React: Memoisierung, Virtualisierung, Code-Splitting und Profiling mit echten Vorher-Nachher-Benchmarks.

Performance ist ein Feature
Langsame Oberflächen kosten Geld. Eine Verzögerung von 100ms bei der Ladezeit kann die Konversionsrate um 1% senken. Ein ruckelndes Scroll-Erlebnis vertreibt Nutzer. Performance ist kein nettes Extra — sie ist eine Produktanforderung.
Falsch angegangene Optimierung verschwendet jedoch Entwicklungszeit und erhöht die Komplexität, ohne einen messbaren Nutzen zu bringen. Dieser Leitfaden konzentriert sich auf Änderungen, die wirklich etwas bewirken.
Schritt 0: Profiling vor der Optimierung
Jede Performance-Verbesserung sollte mit einer Messung beginnen. Der React DevTools Profiler und der Performance-Tab in Chrome sind dabei deine besten Freunde.
// Enable profiling in production builds
// next.config.ts
const nextConfig = {
experimental: {
reactCompiler: true, // React 19+
},
productionBrowserSourceMaps: true,
};Zeichne eine Performance-Trace auf, identifiziere die Komponenten mit den längsten Renderzeiten und behebe zuerst diese. Eine Komponente zu optimieren, die in 2ms rendert, ist verschwendete Mühe.
Pattern 1: Memoisierung — chirurgisch präzise einsetzen
React.memo, useMemo und useCallback haben ihren Preis: Die Vergleichsfunktion läuft bei jedem Rendern. Memoisierung lohnt sich nur, wenn die Komponente teuer UND die Props stabil sind.
// ❌ Over-memoizing cheap components — adds overhead, no benefit
const SimpleLabel = React.memo(({ text }: { text: string }) => (
<span>{text}</span>
));
// ✅ Memoize expensive computations
const DataGrid = React.memo(({ rows, columns }: DataGridProps) => {
return <VirtualizedTable rows={rows} columns={columns} />;
}, (prev, next) => {
// Custom comparison — shallow equality isn't enough for arrays
return prev.rows === next.rows && prev.columns === next.columns;
});
// ✅ Stabilize callbacks passed to memoized children
function Dashboard({ userId }: { userId: string }) {
const handleRowClick = useCallback((rowId: string) => {
router.push(`/items/${rowId}`);
}, []); // No dependencies — stable across renders
return <DataGrid rows={rows} onRowClick={handleRowClick} />;
}Faustregel: Memoisiere nur, wenn du die Verbesserung messen kannst.
Pattern 2: Virtualisierung für lange Listen
10,000 DOM-Knoten zu rendern ist der schnellste Weg, die Performance zu ruinieren. Virtualisierung rendert nur die sichtbaren Elemente.
import { useVirtualizer } from "@tanstack/react-virtual";
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 64, // estimated row height in px
overscan: 5, // render 5 extra items above/below viewport
});
return (
<div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative" }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: "absolute",
top: 0,
transform: `translateY(${virtualItem.start}px)`,
width: "100%",
height: `${virtualItem.size}px`,
}}
>
<ListRow item={items[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}Bei 10,000 Elementen sinkt die Renderzeit von ~8 Sekunden auf ~50ms. Das ist keine marginale Verbesserung — das ist transformativ.
Pattern 3: Code-Splitting und Lazy Loading
Jedes Kilobyte JavaScript blockiert das Rendering. Teile dein Bundle an Routen- und Komponentengrenzen auf.
import { lazy, Suspense } from "react";
// Route-level splitting — Next.js handles this automatically
// Component-level splitting for heavy, conditionally rendered components
const RichTextEditor = lazy(() => import("@/components/RichTextEditor"));
const DataVisualization = lazy(() => import("@/components/DataVisualization"));
function ArticleEditor({ article }: { article: Article }) {
const [showPreview, setShowPreview] = useState(false);
return (
<div>
<Suspense fallback={<EditorSkeleton />}>
<RichTextEditor content={article.content} />
</Suspense>
{showPreview && (
<Suspense fallback={<div>Loading preview...</div>}>
<DataVisualization data={article.metrics} />
</Suspense>
)}
</div>
);
}In Kombination mit next/dynamic erhältst du eine sehr feingranulare Kontrolle darüber, was im initialen Bundle landet.
Pattern 4: State lokal halten
Globaler State verursacht globale Re-Renders. Die Lösung ist oft architektonischer Natur, nicht bloß ein Aufruf der React-API.
// ❌ Storing UI state in a global store — every subscriber re-renders
const useStore = create((set) => ({
isDropdownOpen: false,
toggleDropdown: () => set((s) => ({ isDropdownOpen: !s.isDropdownOpen })),
}));
// ✅ Keep UI state local to the component that owns it
function SearchBar() {
const [isOpen, setIsOpen] = useState(false); // Only SearchBar re-renders
return (
<div>
<input onFocus={() => setIsOpen(true)} onBlur={() => setIsOpen(false)} />
{isOpen && <SearchSuggestions />}
</div>
);
}Frage dich: „Wer muss diesen State eigentlich kennen?" Wenn die Antwort eine einzelne Komponente ist, halte ihn lokal.
Pattern 5: Transitions für nicht dringende Updates
Der Hook useTransition in React 18 markiert Updates als nicht dringend und hält die UI so auch bei teuren Rendervorgängen reaktionsfähig.
import { useTransition, useState } from "react";
function SearchPage() {
const [query, setQuery] = useState("");
const [results, setResults] = useState<Result[]>([]);
const [isPending, startTransition] = useTransition();
function handleSearch(value: string) {
setQuery(value); // Urgent: update input immediately
startTransition(() => {
// Non-urgent: can be interrupted if user types again
setResults(filterLargeDataset(value));
});
}
return (
<div>
<input value={query} onChange={(e) => handleSearch(e.target.value)} />
{isPending && <Spinner />}
<ResultList results={results} />
</div>
);
}Das Ergebnis: Das Eingabefeld bleibt flüssig, während die Ergebnisliste im Hintergrund berechnet wird.
Erfolg messen
Bevor du eine Optimierung ausrollst:
| Metrik | Vorher | Nachher | Tool |
|---|---|---|---|
| Time to Interactive | 4.2s | 1.8s | Lighthouse |
| Largest Contentful Paint | 3.1s | 1.2s | Chrome DevTools |
| Renderzeit der Komponente | 320ms | 18ms | React Profiler |
| Bundle-Größe (initial) | 840KB | 280KB | next build |
Wenn du diese Tabelle nicht ausfüllen kannst, ist die Optimierung noch nicht abgeschlossen.
Die goldene Regel
Performance-Arbeit, die tatsächlich ausgeliefert wird, ist mehr wert als perfekte Arbeit, die es nie tut. Beginne mit den größten Gewinnen — Virtualisierung und Code-Splitting bringen typischerweise 10x-Verbesserungen. Memoisierung macht die letzten 10% aus.


