Context löst Prop-Drilling — aber wenn der value-Prop sich ändert, rendern alle Komponenten neu, die useContext für diesen Context aufrufen. Auch wenn sie den geänderten Teil gar nicht nutzen. Das ist der Hauptperformance-Engpass bei Context-heavy Apps. Drei Werkzeuge helfen: useMemo für stabile value-Referenzen, Context-Splitting (mehrere kleine Contexts statt einem großen), und der Wechsel zu einem Selector-basierten Store (Zustand, Jotai, Redux) bei hochfrequenten Updates. Dieser Artikel zeigt die typischen Fallen und ihre Lösungen.
Das Grundproblem
const AppContext = createContext(null);
function Provider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [cart, setCart] = useState([]);
// FALLE: bei JEDEM Render ein neues Object
return (
<AppContext.Provider value={{ user, theme, cart, setUser, setTheme, setCart }}>
{children}
</AppContext.Provider>
);
}
function ThemeButton() {
const { theme, setTheme } = useContext(AppContext);
// rendert auch, wenn cart oder user wechseln — nicht nur bei theme
return <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>{theme}</button>;
}Wenn jemand etwas in den Cart legt, rendert ThemeButton mit — obwohl er nur Theme braucht.
Lösung 1: useMemo für value-Stabilität
function Provider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const value = useMemo(
() => ({ user, theme, setUser, setTheme }),
[user, theme]
);
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
}useMemo stellt sicher, dass value dieselbe Referenz behält, solange die Dependencies sich nicht ändern. Aber: das löst nur die Fälle, in denen der Provider selbst aus einem anderen Grund rendert (z.B. weil sein Eltern rendert). Wenn user oder theme sich ändern, kommt ohnehin ein neues value-Object — und alle Konsumenten rendern.
Lösung 2: Context-Splitting
Statt einem großen Context mehrere kleine — jeder mit eigenem Update-Zyklus:
const UserContext = createContext(null);
const ThemeContext = createContext(null);
const CartContext = createContext(null);
function Providers({ children }) {
return (
<UserProvider>
<ThemeProvider>
<CartProvider>
{children}
</CartProvider>
</ThemeProvider>
</UserProvider>
);
}
// ThemeButton hört nur auf ThemeContext — Cart-Updates rendern ihn NICHT
function ThemeButton() {
const { theme, setTheme } = useContext(ThemeContext);
return <button onClick={() => setTheme(...)}>{theme}</button>;
}Jeder Context hat seinen eigenen Provider mit eigenem State. Konsumenten rendern nur, wenn ihr spezifischer Context sich ändert. Das ist der wichtigste Performance-Hebel bei Context.
Lösung 3: State + Dispatch getrennt
Wenn state selten gebraucht wird, aber dispatch/Setter oft, lohnt sich das Aufsplitten:
const CountStateContext = createContext(0);
const CountDispatchContext = createContext(() => {});
function CountProvider({ children }) {
const [count, setCount] = useState(0);
return (
<CountStateContext.Provider value={count}>
<CountDispatchContext.Provider value={setCount}>
{children}
</CountDispatchContext.Provider>
</CountStateContext.Provider>
);
}
// Komponente, die NUR den Setter braucht
function IncrementButton() {
const setCount = useContext(CountDispatchContext);
// rendert NIE neu, wenn count sich ändert — setCount ist stabil
return <button onClick={() => setCount(c => c + 1)}>+1</button>;
}setCount aus useState ist stabil — CountDispatchContext ändert seinen Wert nie. Der Button rendert nur einmal beim Mount.
Lösung 4: Externer Store mit Selectors
Bei vielen Konsumenten mit partiellen Interessen ist Context strukturell ungeeignet — kein Selector-Mechanismus. Libraries wie Zustand, Jotai oder Redux lösen das:
import { create } from 'zustand';
const useStore = create((set) => ({
user: null,
theme: 'light',
cart: [],
setTheme: (t) => set({ theme: t }),
setUser: (u) => set({ user: u }),
addToCart: (item) => set(s => ({ cart: [...s.cart, item] })),
}));
function ThemeButton() {
// Selector — rendert NUR bei theme-Wechsel
const theme = useStore(s => s.theme);
const setTheme = useStore(s => s.setTheme);
return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>{theme}</button>;
}Zustand-Hook unterstützt Selectors. Komponenten subscriben nur an den Teil-State, den sie lesen — Cart-Updates rendern ThemeButton nicht.
Lösung 5: React.memo auf Kindern
React.memo allein hilft NICHT — Context umgeht den memo-Vergleich. Aber in Kombination mit Context-Splitting kann es zusätzliche unnötige Renders verhindern, wenn der Eltern aus anderem Grund rendert.
const Display = memo(function Display({ value }) {
return <span>{value}</span>;
});
function Container() {
const { theme } = useContext(ThemeContext);
return <Display value={theme} />;
}Wenn Container aus irgendeinem Grund rendert, aber theme unverändert ist, rendert Display nicht neu (Props-Vergleich gleich). Hilft aber NICHT, wenn theme sich ändert — dann rendert sowohl Container als auch Display.
Entscheidungs-Baum
- Wie oft ändert sich der Wert? Selten (Theme, Locale, Auth): Context.
- Wie viele Konsumenten? Wenige (5-10): Context. Viele (50+): Selector-basiert.
- Brauchen Konsumenten alle Teile? Ja: ein Context. Nein, partiell: Context-Splitting oder externer Store.
- Hochfrequente Updates (>10/sec)? Externer Store oder lokaler State.
- Server-State (API)? TanStack Query, SWR — nicht Context.
Häufige Stolperfallen
value-Prop mit Inline-Object — neue Referenz pro Render.
<Provider value={{a, b}}> ist der häufigste Performance-Bug bei Context. Lösung: useMemo oder Werte stabil halten.
React.memo hilft NICHT gegen Context-Updates.
Context-Konsumenten rendern bei value-Wechsel IMMER, egal ob memo. React.memo hilft nur gegen Eltern-Re-Renders ohne Context-Änderung.
Context-Splitting ist der wichtigste Performance-Hebel.
Statt einem großen Mega-Context lieber mehrere kleine. Konsumenten subscriben nur das, was sie brauchen. Verwaltungs-Overhead minimal, Performance-Gewinn deutlich.
State + Dispatch trennen, wenn Setter weit verbreitet sind.
Bei vielen IncrementButton-artigen Komponenten, die nur den Setter brauchen: separater DispatchContext. Setter sind stabil → kein Re-Render bei State-Wechsel.
useReducer liefert stabilen dispatch.
Der dispatch aus useReducer ist stabil zwischen Renders — perfekt für einen DispatchContext. Konsumenten rendern bei State-Wechsel nicht neu, solange sie nur dispatch abrufen.
Bei vielen Konsumenten + hochfrequenten Updates: externer Store.
Zustand, Jotai, Redux mit useSelector. Diese Libraries haben Selector-Mechanismen, die feiner als Context arbeiten. Pre-Hooks war Redux notwendig — heute gibt es leichtgewichtigere Optionen.
Server-State (API-Daten) gehört NICHT in Context.
Caching, Stale-While-Revalidate, Mutations, Pagination — alles Themen, die TanStack Query/SWR spezifisch lösen. Context wäre nur ein primitiver Cache mit Re-Render-Lawinen.
Provider-Hell durch Komposition-Komponente auflösen.
Viele verschachtelte Provider werden unleserlich. Eine AppProviders-Komponente, die alle wraps, hält den App-Root sauber.
Weiterführende Ressourcen
Externe Quellen
- Passing Data Deeply with Context – react.dev
- Scaling Up with Reducer and Context – react.dev
- Zustand Documentation
- Jotai Documentation
- Redux Toolkit – Redux Style Guide
- TanStack Query