React Performance messen bevor optimieren: Profiler, DevTools und Metriken
AI generated
</>
{ }
React · Performance · Profiler · DevTools · Web Vitals
React Performance messen
bevor optimieren — Profiler & Metriken

useMemo und useCallback überall einsetzen macht React-Apps nicht schneller — es macht sie komplizierter. Wer zuerst misst, findet die echten Engpässe: unnötige Re-Renders, teure Berechnungen an der falschen Stelle und Layout-Thrashing. Dieser Artikel zeigt den richtigen Ablauf mit React Profiler, Chrome DevTools und handfesten Metriken.

16 Min. Lesezeit React Profiler · useMemo · useCallback · memo · Web Vitals React 18 · Chrome DevTools · Lighthouse

1. Warum Messen vor Optimieren entscheidend ist

Der häufigste Fehler bei der React-Performance-Optimierung ist das Optimieren ohne Datenbasis. Entwickler wickeln Berechnungen in useMemo, Callbacks in useCallback und Komponenten in React.memo — weil es sich anfühlt wie Optimierung. In Wirklichkeit fügen sie Komplexität hinzu, ohne zu wissen, ob die betroffenen Stellen überhaupt Performance-Engpässe sind. Das Ergebnis: schwerer lesbare Komponenten, höhere Gefahr von Bugs durch veraltete Closures, und kein messbarer Geschwindigkeitsgewinn.

Der korrekte Ablauf ist umgekehrt: Zuerst misst man mit dem React Profiler, welche Komponenten warum und wie oft re-rendern. Dann prüft man, ob diese Re-Renders tatsächlich zu sichtbaren Leistungsproblemen führen. Erst wenn beide Fragen mit "ja" beantwortet sind, greift man zur Optimierung — und misst danach erneut, ob sie geholfen hat. Dieser datengetriebene Ansatz ist der Unterschied zwischen Ingenieurarbeit und Kaffeesatzlesen.

2. React DevTools Profiler: Grundlagen und Bedienung

Der React Profiler ist in den React DevTools Browser-Extensions eingebaut und der wichtigste Startpunkt für jede React-Performance-Analyse. Um ihn zu nutzen, öffnet man die DevTools, navigiert zum Tab "Profiler" und startet eine Aufzeichnung mit dem Record-Button. Dann führt man die Aktion aus, die sich langsam anfühlt — ein Klick, eine Eingabe, ein Seitennavigation — und stoppt die Aufzeichnung. Der Profiler zeigt dann einen Flamegraph aller Render-Operationen in der aufgezeichneten Zeitspanne.

Im Flamegraph zeigt jeder Block eine Komponente. Die Breite entspricht der Zeit, die der React-Reconciler für diese Komponente aufgewendet hat. Graue Blöcke bedeuten, dass die Komponente nicht re-gerendert wurde. Farbige Blöcke (von gelb bis rot) zeigen Re-Renders, wobei die Farbe die relative Render-Zeit anzeigt. Besonders wertvoll ist die Einstellung "Record why each component rendered": Sie zeigt pro Komponente, ob sie durch eine Prop-Änderung, eine State-Änderung oder einen Context-Update re-gerendert wurde — das ist die präziseste Diagnosehilfe, die React bietet.


// Profiler API for programmatic performance measurement
import { Profiler, ProfilerOnRenderCallback } from 'react';

// Callback receives render timing data for each commit
const onRender: ProfilerOnRenderCallback = (
  id,           // component tree identifier
  phase,        // "mount" or "update"
  actualDuration,  // time spent rendering (ms)
  baseDuration,    // estimated time without memoization
  startTime,
  commitTime
) => {
  // Log to analytics or console — only in development
  if (process.env.NODE_ENV === 'development') {
    console.table({ id, phase, actualDuration, baseDuration });
  }
  // In production: send to observability system
  if (actualDuration > 16) {
    analytics.track('slow_render', { id, phase, actualDuration });
  }
};

// Wrap the subtree you want to measure
export const TrackedProductList = () => (
  <Profiler id="ProductList" onRender={onRender}>
    <ProductList />
  </Profiler>
);

3. Unnötige Re-Renders identifizieren

React re-rendert eine Komponente, wenn ihr State, ihre Props oder ein von ihr konsumierter Context sich geändert haben. Das ist das erwartete Verhalten. Problematisch werden Re-Renders, wenn sie kaskadenweise Kindkomponenten erfassen, die sich gar nicht geändert haben — oder wenn ein Context-Update eine ganze Komponentenbaumeene rendert, obwohl nur ein kleiner Teil tatsächlich von der Änderung betroffen ist. Der React Profiler zeigt genau das: Komponenten, die ohne erkennbaren eigenen Grund re-rendern, weil ein Elternteil neu gerendert hat.

Ein besonders häufiges Muster: Callback-Funktionen, die bei jedem Render des Elternteils als neue Referenz übergeben werden. Da React Props per Referenz vergleicht, sieht eine Kindkomponente eine "geänderte" Prop, obwohl die Funktion semantisch identisch ist. Das führt zu Re-Renders der Kindkomponente bei jedem Render des Elternteils — selbst wenn die Kindkomponente mit React.memo umwickelt ist. Das Identifizieren dieser Muster im Profiler ist der erste Schritt, bevor man entscheidet, ob useCallback hier der richtige Ansatz ist.

4. React.memo: wann es hilft und wann es schadet

React.memo umwickelt eine Komponente und verhindert ihr Re-Render, wenn ihre Props sich referenziell nicht geändert haben. Das klingt nach einem einfachen Gewinn — ist es aber nicht immer. Der referenzielle Vergleich hat selbst einen kleinen Overhead. Bei einfachen, schnell rendernden Komponenten ist dieser Overhead größer als das gesparte Re-Render. React.memo lohnt sich nur, wenn zwei Bedingungen gleichzeitig erfüllt sind: Die Komponente rendert oft, und das Re-Render ist messbar teuer.

Ein häufiger Fehler: React.memo um eine Komponente legen, der Elternteil übergibt aber bei jedem Render Objekte oder Arrays als Props, die inline erstellt werden — style={ { margin: 0 } }, items={[a, b]}. Diese sind bei jedem Render eine neue Referenz, also wird React.memo niemals den Re-Render verhindern. Der Fix liegt nicht in mehr Memoization, sondern darin, solche Props entweder zu stabilisieren (mit useMemo) oder die Komponentenstruktur so anzupassen, dass die Daten nicht als Inline-Literale übergeben werden müssen.


import React, { memo, useMemo, useCallback, useState } from 'react';

// BAD: memo is useless because items is a new array on every render
const BadExample = () => {
  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      {/* items is a new reference every render — memo will not help */}
      <MemoizedList items={[1, 2, 3]} />
    </>
  );
};

// GOOD: stabilize the array with useMemo so memo can do its job
const GoodExample = () => {
  const [count, setCount] = useState(0);
  // items reference only changes when dependencies change
  const items = useMemo(() => [1, 2, 3], []);
  const handleSelect = useCallback((id: number) => {
    console.log('selected', id);
  }, []);

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      {/* memo now works: items and handleSelect are stable */}
      <MemoizedList items={items} onSelect={handleSelect} />
    </>
  );
};

// memo with custom comparison — only re-render if id changes
const MemoizedList = memo(
  ({ items, onSelect }: { items: number[]; onSelect: (id: number) => void }) => (
    <ul>{items.map(i => <li key={i} onClick={() => onSelect(i)}>{i}</li>)}</ul>
  )
);

5. useMemo für teure Berechnungen

useMemo speichert das Ergebnis einer Berechnung zwischen Renders und berechnet es nur neu, wenn sich die Abhängigkeiten ändern. Die entscheidende Frage ist: Wie teuer ist die Berechnung wirklich? Bei einfachen Operationen wie Filtern eines kleinen Arrays, Formatieren eines Datums oder Sortieren von zehn Elementen ist der Overhead von useMemo selbst (Dependency-Vergleich, Cache-Lookup) oft größer als die gesparte Berechnung. useMemo lohnt sich messbar bei Operationen, die im Profiler mehr als 1–2 ms kosten oder bei Berechnungen, die eine neue Objektreferenz erzeugen, die als Prop weitergegeben wird.

Ein besonders wichtiger Anwendungsfall: Context-Werte. Wenn ein Context-Provider seinen Wert als Objekt-Literal bereitstellt — value={ { user, login, logout } } — ist dieser Wert bei jedem Render des Providers eine neue Referenz, und alle Consumer re-rendern, egal ob sich user, login oder logout geändert hat. Das richtige Muster ist useMemo(() => ({ user, login, logout }), [user, login, logout]). Der Context triggert dann nur Re-Renders, wenn sich tatsächlich ein Wert geändert hat — und nicht bei jedem Re-Render des Provider-Elternteils.

6. useCallback: stabile Referenzen für Props

useCallback gibt dieselbe Funktionsreferenz zurück, solange sich die Abhängigkeiten nicht ändern. Sein primärer Nutzen liegt darin, Callbacks stabil zu machen, damit memoizierte Kindkomponenten nicht unnötig re-rendern. Ohne useCallback erstellt jeder Render des Elternteils eine neue Funktionsreferenz — und React.memo in der Kindkomponente verhindert den Re-Render dann nicht. Mit useCallback bleibt die Referenz stabil, und React.memo kann seinen Job tun.

Ein fortgeschrittener Anwendungsfall ist das Stabilisieren von Callbacks für useEffect. Wenn ein Callback in der Dependency-Liste eines useEffect steht und sich bei jedem Render ändert, läuft der Effect bei jedem Render — das ist fast immer ein Bug. useCallback stabilisiert die Referenz und macht den Effect deterministisch. Alternativ empfiehlt sich seit React 18 das Muster mit useEffectEvent (ab React 19 stabil), das den Callback aus der Dependency-Liste herausnimmt und so den Effect nur noch auf tatsächlich relevante Dependencies reagieren lässt.

7. Web Vitals und Lighthouse richtig lesen

Web Vitals sind nutzerzentrierte Metriken, die direkt mit der wahrgenommenen Performance korrelieren. LCP (Largest Contentful Paint) misst, wann das größte sichtbare Element geladen ist — Zielwert unter 2,5 Sekunden. INP (Interaction to Next Paint) hat FID abgelöst und misst die Zeit zwischen Nutzerinteraktion und dem nächsten Frame — Zielwert unter 200ms. INP ist die Metrik, die am direktesten durch schlechte React-Performance beeinflusst wird: Wenn ein Click-Handler zu einem langen Synchron-Render führt, steigt INP sofort. CLS (Cumulative Layout Shift) misst visuelle Instabilität — Bilder ohne Dimensionen oder asynchron geladene Inhalte ohne Platzhalter sind die häufigsten Ursachen.

Lighthouse und Chrome DevTools Performance-Tab liefern zwei verschiedene Perspektiven. Lighthouse simuliert einen langsameren Browser und gibt eine einzige Score — gut für den Vergleich vor und nach Optimierungen. Der Performance-Tab zeichnet den tatsächlichen Browser-Thread auf und zeigt, wo Long Tasks den Haupt-Thread blockieren. Long Tasks über 50ms sind der direkte Feind niedrigen INP-Scores. In React entstehen sie typischerweise durch synchrone State-Updates, die viele Komponenten gleichzeitig neu rendern. React 18's startTransition kennzeichnet nicht-dringende Updates als niedrig priorisiert und lässt den Browser dazwischen Nutzerinteraktionen verarbeiten.

8. Lange Listen mit Virtualisierung

Das Rendern von tausenden DOM-Elementen ist einer der wenigen React-Performance-Probleme, bei dem keine Memoization hilft. Der DOM hat eine fundamentale Kostenfunktion: Jedes Element nimmt Layout-Zeit, Paint-Zeit und Speicher. Eine Liste mit 10.000 Produkten rendert 10.000 DOM-Knoten, auch wenn der Nutzer nur 20 davon sieht. Virtualisierung löst das, indem sie nur die sichtbaren Elemente rendert und den Rest durch Platzhalter ersetzt. Beim Scrollen werden unsichtbar gewordene Elemente entfernt und neu sichtbare erstellt.

Die empfohlenen Libraries sind @tanstack/react-virtual (headless, vollständige Kontrolle über Markup) und react-window (einfacher, aber weniger flexibel). Für komplexere Anforderungen wie variable Elementhöhen, horizontales Scrolling oder Gruppen bietet react-virtuoso die beste Developer Experience. Wichtig: Virtualisierung hat selbst Overhead — für Listen unter 100 Elementen ist sie kontraproduktiv. Der Profiler zeigt, ob die render-Zeit der Liste tatsächlich ein Problem ist, bevor man Virtualisierung einführt.


// Virtualizing a large product list with @tanstack/react-virtual
import React, { useRef } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';

interface Product { id: string; name: string; price: number; }

interface VirtualProductListProps {
  products: Product[];
}

export const VirtualProductList: React.FC<VirtualProductListProps> = ({ products }) => {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: products.length,
    getScrollElement: () => parentRef.current,
    // Provide estimated height — virtualizer corrects after first render
    estimateSize: () => 80,
    overscan: 5, // render 5 extra items above/below viewport
  });

  return (
    <div ref={parentRef} style={ { height: '600px', overflow: 'auto' } }>
      {/* Total scrollable height — maintains correct scrollbar */}
      <div style={ { height: virtualizer.getTotalSize(), position: 'relative' } }>
        {virtualizer.getVirtualItems().map(virtualItem => (
          <div
            key={virtualItem.key}
            data-index={virtualItem.index}
            ref={virtualizer.measureElement}
            style={ {
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              transform: `translateY(${virtualItem.start}px)`,
            } }
          >
            <ProductCard product={products[virtualItem.index]} />
          </div>
        ))}
      </div>
    </div>
  );
};

9. Optimierungsstrategien im Vergleich

Nicht jede Performance-Optimierung hilft gleich viel. Die Wahl der richtigen Strategie hängt davon ab, was der Profiler als Engpass identifiziert hat. Blindes Anwenden aller Optimierungen gleichzeitig macht den Code schwerer wartbar, ohne garantierten Gewinn.

Problem (Profiler zeigt…) Falsche Reaktion Richtige Strategie Messung
Kindkomponente rendert oft memo ohne stabile Props useCallback + memo zusammen Profiler: actualDuration sinkt
Context löst viele Re-Renders aus Inline-Objekt im Provider useMemo für Context-Wert Profiler: weniger Render-Commits
Liste mit 1000+ Elementen memo auf jedem List-Item Virtualisierung (react-virtual) DevTools: DOM-Node-Anzahl
INP > 200ms nach Klick useMemo überall startTransition für Updates Web Vitals: INP-Score
Teurer Initial-Load Alles memoizieren Code-Splitting + lazy() Lighthouse: LCP-Score

Die wichtigste Erkenntnis aus der Tabelle: Es gibt keine universelle Optimierung. Jede Strategie adressiert ein spezifisches Problem, das der Profiler vorher identifiziert haben muss. Wer alle Strategien gleichzeitig anwendet, produziert komplexen Code, der schwerer zu testen und debuggen ist — ohne zwingend schneller zu sein. Der Ablauf ist immer: Messen, Problem identifizieren, Strategie wählen, Optimierung umsetzen, erneut messen.

Mironsoft

React-Performance-Analyse und gezielte Optimierung

React-App fühlt sich langsam an?

Wir führen eine datengetriebene Performance-Analyse eurer React-Anwendung durch — mit Profiler, Web Vitals und Chrome DevTools — und liefern priorisierte Optimierungsmaßnahmen mit messbarem Ergebnis.

Profiler-Analyse

Re-Render-Kaskaden, teure Berechnungen und Context-Overhead identifizieren

Gezielte Optimierung

memo, useMemo, useCallback und startTransition genau dort, wo sie helfen

Web-Vitals-Verbesserung

INP, LCP und CLS verbessern — mit vor/nach Messungen als Nachweis

10. Zusammenfassung

Der wichtigste Grundsatz der React-Performance-Optimierung lautet: Messen zuerst, optimieren danach. Der React Profiler in den DevTools zeigt, welche Komponenten re-rendern, warum sie das tun und wie lange es dauert. React.memo lohnt sich nur mit stabilen Props-Referenzen — was useCallback für Funktionen und useMemo für Objekte/Arrays liefert. Context-Werte müssen mit useMemo stabilisiert werden, damit nicht bei jedem Provider-Render alle Consumer re-rendern. Lange Listen brauchen Virtualisierung, nicht Memoization. Web Vitals — insbesondere INP — sind die einzige nutzerzentrierte Bestätigung, dass eine Optimierung tatsächlich geholfen hat.

Die häufigste Fehlinvestition in React-Performance ist überall useMemo und useCallback anzuwenden, bevor der Profiler überhaupt geöffnet wurde. Beide APIs haben einen kleinen eigenen Overhead und machen Code schwerer lesbar. Sie sind Werkzeuge für spezifische, gemessene Probleme — keine allgemeinen Präventivmaßnahmen. Teams, die mit dem Profiler arbeiten, finden regelmäßig, dass ihre vermeintlichen Performance-Probleme sich auf zwei oder drei konkrete Stellen konzentrieren, die mit gezielten Maßnahmen fix behebbar sind.

React Performance — Das Wichtigste auf einen Blick

Profiler zuerst

React DevTools Profiler mit "Record why each component rendered" aufzeichnen — Engpässe finden, bevor man optimiert.

memo + useCallback zusammen

React.memo ohne stabile Props-Referenzen (useCallback/useMemo) bringt nichts — beide müssen gemeinsam eingesetzt werden.

Context-Werte stabilisieren

Inline-Objekte im Provider erzeugen neue Referenzen bei jedem Render. useMemo für Context-Werte verhindert massenhaft Re-Renders.

INP als Leitmetrik

Interaction to Next Paint unter 200ms — startTransition für nicht-dringende Updates verhindert Long Tasks auf dem Haupt-Thread.

11. FAQ: React Performance messen und optimieren

1Wann useMemo einsetzen?
Wenn der Profiler eine Berechnung als teuer ausweist (>1–2ms) oder das Ergebnis als Prop an eine memoizierte Kindkomponente geht und stabile Referenz braucht.
2Was zeigt der React Profiler?
Flamegraph mit Render-Dauer, welche Komponenten re-rendert haben und warum (Props/State/Context). Option "Record why" aktivieren.
3Warum hilft React.memo manchmal nicht?
Props sind neue Referenzen bei jedem Render (Objekte, Callbacks inline). memo vergleicht referenziell — neue Referenz = immer Re-Render.
4Was ist INP und warum React-relevant?
Interaction to Next Paint: Zeit zwischen Nutzerinteraktion und nächstem Frame. Synchrone React-Updates blockieren den Thread — startTransition priorisiert herunter.
5Ab wann Listen virtualisieren?
Ab ~100–200 Elementen, wenn der Profiler die Liste als Engpass zeigt. Darunter überwiegt der Virtualisierungs-Overhead.
6Unterschied useMemo vs. useCallback?
useMemo memoiziert ein Ergebnis, useCallback eine Funktion. useCallback(fn, deps) ist useMemo(() => fn, deps) — beide geben stabile Referenzen zurück.
7Web Vitals in React messen?
Paket web-vitals: onINP, onLCP, onCLS importieren und an Analytics senden. Chrome DevTools Performance-Tab zeigt Long Tasks und INP-Kandidaten.
8Was macht startTransition?
Markiert Updates als nicht-dringend. React rendert Nutzereingaben sofort, verzögert startTransition-Updates bis der Browser Zeit hat — INP bleibt niedrig.
9Performance-Messung in Produktion?
Profiler-API aus React: onRender-Callbacks laufen auch in Produktion und können Timings an Monitoring senden. React DevTools nur in Development.
10Häufigste Ursachen schlechter React-Performance?
Context-Inline-Objekte, Callback-Instabilität, zu viele DOM-Elemente in Listen, synchrone Render-Berechnungen, fehlendes Code-Splitting.