Context Selectors: Unnötige Re-Renders durch Context-Splitting vermeiden
AI generated
{ }
React 19 · Performance · State Management
Context Selectors
Unnötige Re-Renders durch Context-Splitting vermeiden

Ein einzelner großer Context wirkt praktisch, bis jede Komponente bei jeder noch so kleinen Änderung neu rendert. Context-Splitting und echte Selektoren lösen das Problem strukturell statt symptomatisch.

15 Min. Lesezeit Context API Re-Render-Analyse State-Architektur

1. Das Problem: Ein Context, viele Consumer, jede Änderung trifft alle

React löst useContext nicht selektiv auf. Sobald sich der Wert, den ein Provider übergibt, ändert, rendert React jede Komponente neu, die diesen Context per useContext konsumiert, unabhängig davon, ob die Komponente den geänderten Teil des Werts überhaupt verwendet. Ein Context, der beispielsweise user, theme und notifications gemeinsam bündelt, sorgt dafür, dass eine Komponente, die nur theme liest, bei jeder neuen Benachrichtigung trotzdem neu rendert.

Das Muster fällt in kleinen Anwendungen kaum auf, wird aber mit wachsender Komponentenzahl schnell zum echten Performance-Problem. Besonders tückisch ist, dass der Effekt sich nicht in offensichtlichen Fehlern zeigt, sondern in diffuser Trägheit: Interaktionen fühlen sich langsamer an, die React DevTools Profiler-Ansicht zeigt einen Wald aus grau eingefärbten, unnötig gerenderten Komponenten, und niemand findet auf den ersten Blick die Ursache, weil der Code funktional korrekt ist.

2. Ein großer Context als Ausgangspunkt

Ein typisches Anti-Pattern ist der App-weite Context, der als zentrale Ablage für nahezu jeden globalen Zustand dient. Er ist bequem, weil man nur einen Provider einbindet und überall per useContext(AppContext) Zugriff hat. Genau diese Bequemlichkeit ist die Falle: Jede State-Änderung, egal wie klein, erzeugt ein neues Context-Objekt, und React kann nicht erkennen, dass ein Consumer nur an einem Teilbereich interessiert ist.

Im folgenden Beispiel sieht man den klassischen Aufbau. UserBadge liest ausschließlich user, wird aber bei jeder Änderung von notifications ebenfalls neu gerendert, weil beide Werte im selben Context-Objekt stecken und React am Objekt als Ganzes vergleicht, nicht an dessen einzelnen Feldern.


// AppContext.jsx -- ein Context fuer alles
const AppContext = createContext(null);

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);

  const value = { user, setUser, theme, setTheme, notifications, setNotifications };

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

// UserBadge.jsx -- rendert bei JEDER Aenderung von notifications neu
function UserBadge() {
  const { user } = useContext(AppContext);
  console.log('UserBadge rendert');
  return <span>{user?.name}</span>;
}

3. Warum React nicht selektiv vergleicht

Der Grund liegt in der Funktionsweise von useContext: Der Hook abonniert das gesamte Context-Objekt, nicht einzelne Felder daraus. Ändert sich die Referenz des value-Objekts, das der Provider übergibt, muss React allen abonnierten Komponenten mitteilen, dass sich etwas geändert haben könnte, und sie neu rendern lassen. Ein flacher Vergleich einzelner Felder findet dabei nicht statt, weil der Context selbst keine Vorstellung davon hat, welche Teile seines Werts eine Komponente tatsächlich nutzt.

Das ist kein Bug, sondern bewusstes Design: Die Context API wurde ursprünglich für selten wechselnde Werte wie Theming oder Lokalisierung konzipiert, nicht als vollwertige State-Management-Lösung für häufig wechselnde, granulare Daten. Wird sie trotzdem so eingesetzt, muss man die fehlende Selektivität selbst kompensieren, entweder durch Architektur oder durch zusätzliche Bibliotheken.

4. Context-Splitting als strukturelle Lösung

Die direkteste Lösung ist, den einen großen Context in mehrere kleine, fachlich fokussierte Contexts aufzuteilen. Statt eines AppContext gibt es dann UserContext, ThemeContext und NotificationContext, jeweils mit eigenem Provider und eigenem value-Objekt. Eine Komponente, die nur UserContext konsumiert, wird dann ausschließlich bei Änderungen am Nutzerobjekt neu gerendert, Änderungen an Benachrichtigungen betreffen sie nicht mehr.

Wichtig ist, die Contexts nach Änderungshäufigkeit und fachlicher Zusammengehörigkeit zu trennen, nicht rein nach Datentyp. Werte, die immer gemeinsam geändert werden, dürfen im selben Context bleiben, denn zusätzliche Contexts bedeuten zusätzliche Provider-Verschachtelung und mehr kognitive Last beim Lesen des Baums. Das Ziel ist ein Gleichgewicht: so viele Contexts wie nötig für Renderisolation, so wenige wie möglich für Übersichtlichkeit.


// UserContext.jsx
const UserContext = createContext(null);

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const value = useMemo(() => ({ user, setUser }), [user]);
  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}

// NotificationContext.jsx -- eigener, unabhaengiger Context
const NotificationContext = createContext(null);

function NotificationProvider({ children }) {
  const [notifications, setNotifications] = useState([]);
  const value = useMemo(
    () => ({ notifications, setNotifications }),
    [notifications]
  );
  return (
    <NotificationContext.Provider value={value}>
      {children}
    </NotificationContext.Provider>
  );
}

// UserBadge.jsx -- rendert jetzt NICHT mehr bei Notification-Aenderungen
function UserBadge() {
  const { user } = useContext(UserContext);
  return <span>{user?.name}</span>;
}

5. Memoization der Provider-Werte nicht vergessen

Context-Splitting allein reicht nicht, wenn das value-Objekt bei jedem Render des Providers neu erzeugt wird. Ein Literal wie { user, setUser } direkt im JSX erzeugt bei jedem Render eine neue Objektreferenz, selbst wenn user sich gar nicht geändert hat. React vergleicht Context-Werte per Referenzgleichheit, sodass Consumer trotzdem neu rendern, obwohl der Aufteilungsschritt an sich korrekt war.

Die Lösung ist useMemo um das value-Objekt, mit den tatsächlichen Abhängigkeiten in der Dependency-Liste. Erst dann bleibt die Referenz stabil, solange sich die referenzierten Werte nicht ändern, und Consumer, die den Context nutzen, rendern wirklich nur dann neu, wenn sich fachlich etwas geändert hat. Dieser Schritt wird in der Praxis häufig vergessen und macht viele gut gemeinte Context-Splits wirkungslos.

6. Consumer selbst memoisieren mit React.memo

Selbst mit sauber gesplitteten und memoisierten Contexts kann eine Elternkomponente unnötige Re-Renders an Kindkomponenten weitergeben, wenn diese nicht selbst memoisiert sind. React.memo verhindert, dass eine Kindkomponente neu rendert, solange sich ihre Props nicht ändern, und ergänzt damit die Context-Optimierung um die Komponentenebene.

Kombiniert man Context-Splitting, useMemo für Provider-Werte und React.memo für teure Kindkomponenten, entsteht ein Renderverhalten, das sich präzise an tatsächlichen Datenänderungen orientiert statt an der Struktur des Providerbaums. Wichtig ist, diese Optimierungen gezielt einzusetzen, nicht überall präventiv, denn React.memo hat selbst einen kleinen Vergleichs-Overhead, der sich bei trivialen Komponenten nicht lohnt.


const ExpensiveNotificationList = React.memo(function ExpensiveNotificationList({
  notifications,
}) {
  console.log('ExpensiveNotificationList rendert');
  return (
    <ul>
      {notifications.map((n) => (
        <li key={n.id}>{n.message}</li>
      ))}
    </ul>
  );
});

7. Vergleich zu externen State-Libraries mit echten Selektoren

Bibliotheken wie Zustand oder Jotai lösen das Selektivitätsproblem grundsätzlich anders: Sie erlauben es, gezielt Selektoren zu definieren, die nur den benötigten Teilbereich des Zustands abonnieren. Eine Komponente, die useStore((state) => state.user) aufruft, wird ausschließlich neu gerendert, wenn sich das Ergebnis dieser Selektorfunktion tatsächlich ändert, unabhängig davon, wie viele andere Felder im globalen Store sich parallel ändern.

Das ist ein grundlegend anderer Mechanismus als die Context API: Statt die gesamte Consumer-Baumstruktur bei jeder Wertänderung zu benachrichtigen, prüft die Bibliothek pro Selektor, ob sich der abgeleitete Wert geändert hat, und rendert nur dann. Für Anwendungen mit vielen, häufig wechselnden globalen Zuständen ist das oft die pragmatischere Lösung als immer feineres Context-Splitting, weil die Selektivität nicht durch manuelle Architekturarbeit erkauft werden muss, sondern eingebaut ist.

8. Wann Context-Splitting reicht und wann eine Library sinnvoll ist

Context-Splitting ist die richtige Wahl, wenn der globale Zustand überschaubar ist und sich klar in fachliche Bereiche trennen lässt, etwa Nutzer, Theme und Sprache. Es kommt ohne zusätzliche Abhängigkeit aus, bleibt nah am React-Standardwerkzeugkasten und ist für neue Teammitglieder leicht nachvollziehbar, weil keine zusätzliche API gelernt werden muss.

Sobald der Zustand komplex wird, viele voneinander unabhängige, häufig wechselnde Felder enthält oder plattformübergreifend außerhalb der React-Baumstruktur gelesen werden muss, etwa in Event-Handlern oder Utility-Funktionen, wird eine externe Library mit echten Selektoren attraktiver. Der Umstieg lohnt sich, sobald man merkt, dass man Contexts nur noch aus Performance-Gründen weiter aufsplittet, nicht mehr aus fachlicher Notwendigkeit, denn das ist ein Zeichen, dass die Context API an ihre strukturellen Grenzen stößt.

9. Praxis-Checkliste für Context-Performance

Bevor man an der Context-Struktur schraubt, lohnt sich ein Blick in den React DevTools Profiler: Ein Aufnahme-Durchlauf zeigt zuverlässig, welche Komponenten unnötig neu rendern und ob die Ursache tatsächlich ein Context ist. Ohne diese Messung besteht die Gefahr, Zeit in Optimierungen zu stecken, die keinen messbaren Effekt haben, während das eigentliche Problem woanders liegt, etwa in teuren Berechnungen ohne useMemo.

Die folgende Tabelle fasst die wichtigsten Techniken zusammen und ordnet sie nach Aufwand und Wirkung ein, damit man gezielt entscheiden kann, welcher Schritt für die eigene Codebasis als Nächstes sinnvoll ist.

Technik Aufwand Wirkung Wann einsetzen
useMemo im Provider gering verhindert unnötige Referenzänderungen immer, sobald ein Context-Value ein Objekt ist
Context-Splitting mittel isoliert Re-Renders pro Fachbereich wenn Consumer nur Teilbereiche nutzen
React.memo auf Kindern gering stoppt Weitergabe unveränderter Props bei teuren Kindkomponenten
Externe Library mit Selektoren hoch feingranulare, eingebaute Selektivität bei komplexem, häufig wechselndem globalem State

Mironsoft

React-Architektur, Performance und Magento-Frontend-Integration

React-Frontends, die schnell bleiben statt mit jedem Feature langsamer zu werden?

Wir prüfen bestehende React-Anwendungen auf unnötige Re-Renders, aufgeblähte Bundles und fragile State-Verwaltung und bauen daraus ein Frontend, das performant bleibt und sich sauber an Magento oder andere Backends anbindet.

Performance-Audit

Re-Renders, Bundle-Größe und Ladezeiten systematisch messen und beheben.

State-Architektur

Context, Zustand und Server State sauber trennen statt alles in einen Topf zu werfen.

Magento-Integration

GraphQL- oder REST-Anbindung an Magento robust und typsicher aufbauen.

10. Zusammenfassung

Context-Splitting: Das Wichtigste auf einen Blick

Kernproblem

useContext abonniert das gesamte Objekt, nicht einzelne Felder, daher rendert jede Änderung alle Consumer neu.

Erste Maßnahme

Provider-Value mit useMemo stabilisieren, bevor überhaupt gesplittet wird.

Strukturlösung

Große Contexts in fachlich fokussierte, kleinere Contexts aufteilen.

Alternative

Bei komplexem State: Library mit echten Selektoren wie Zustand oder Jotai einsetzen.

11. FAQ: Context-Splitting: Das Wichtigste auf einen Blick

1Warum rendert eine Komponente neu, obwohl sich der von ihr genutzte Wert im Context gar nicht geändert hat?
Weil useContext das gesamte Context-Objekt abonniert, nicht einzelne Felder. React vergleicht nur die Referenz des value-Objekts, nicht dessen einzelne Eigenschaften, daher lösen auch nicht genutzte Felder ein Re-Render aus.
2Reicht useMemo im Provider allein aus, um das Problem zu lösen?
Es hilft, verhindert aber nur unnötige Referenzänderungen durch Neuerstellung des value-Objekts. Wenn mehrere fachlich unabhängige Werte im selben Context stecken, führt eine Änderung an einem Feld trotzdem zum Re-Render aller Consumer, weshalb zusätzlich Context-Splitting sinnvoll ist.
3Wie erkenne ich, ob Context-Re-Renders tatsächlich ein Performance-Problem in meiner App sind?
Am zuverlässigsten mit dem React DevTools Profiler: Ein Aufnahme-Durchlauf zeigt farblich markiert, welche Komponenten wie oft und wie lange rendern. Erst mit dieser Messung sollte man Optimierungsaufwand investieren.
4Sollte ich jeden Context sofort in mehrere kleine Contexts aufteilen?
Nein, das erzeugt unnötige Provider-Verschachtelung und Komplexität. Splitting lohnt sich erst, wenn tatsächlich unabhängige, unterschiedlich häufig wechselnde Datenbereiche im selben Context stecken.
5Was ist der Unterschied zwischen Context-Splitting und React.memo?
Context-Splitting verhindert, dass ein Consumer überhaupt benachrichtigt wird, wenn sich ein für ihn irrelevanter Teil des globalen Zustands ändert. React.memo verhindert das Re-Render einer Kindkomponente, wenn sich deren Props nicht geändert haben, unabhängig von der Ursache.
6Kann ich Context und eine externe State-Library wie Zustand parallel im selben Projekt nutzen?
Ja, das ist ein gängiges Muster. Context eignet sich weiterhin gut für selten wechselnde, strukturelle Werte wie Theme oder Sprache, während eine Library mit Selektoren für häufig wechselnden, komplexen Anwendungszustand übernimmt.
7Warum ist die Context API ursprünglich nicht für häufig wechselnde Daten gedacht?
Sie wurde primär entwickelt, um das sogenannte Prop-Drilling für selten wechselnde, global relevante Werte zu vermeiden, nicht als vollständige State-Management-Lösung. Die fehlende eingebaute Selektivität ist eine direkte Folge dieses ursprünglichen Anwendungsfalls.
8Verursacht zu viel Context-Splitting selbst Probleme?
Ja, zu viele Provider führen zu tief verschachtelten Komponentenbäumen, die schwerer zu lesen und zu debuggen sind. Es lohnt sich, die Anzahl der Contexts an der tatsächlichen fachlichen Trennung auszurichten statt an rein technischer Renderoptimierung.
9Funktioniert React.memo automatisch bei jedem Context-Consumer?
Nein, React.memo vergleicht nur Props, nicht Context-Werte. Eine Komponente, die per useContext direkt auf einen Context zugreift, rendert bei jeder Context-Änderung neu, selbst wenn sie mit React.memo umschlossen ist.
10Ab welcher Projektgröße lohnt sich der Umstieg von Context auf eine Library mit Selektoren?
Eine feste Grenze gibt es nicht, aber ein deutliches Signal ist, wenn man Contexts nur noch aus Performance-Gründen weiter aufteilt statt aus fachlicher Notwendigkeit. Das zeigt, dass die eingebaute Selektivität einer Library den Aufwand rechtfertigt.