Day 25: React Context Optimization & Re-render Profiling
Diagnose and fix the #1 real-world React performance complaint: Context causing far more re-renders than expected.
Study
Concepts
Every Context consumer re-renders on ANY value change
When a Context Provider's `value` changes (by reference — a new object/array/function on every render counts as "changed" even if the contents look the same), EVERY component that calls `useContext` for that Context re-renders, regardless of whether it actually reads the specific field that changed. This is unlike prop drilling, where a component only re-renders if the specific prop it receives changes.
A very common mistake is passing an inline object literal as the value: `<MyContext.Provider value={{ user, theme }}>` creates a brand-new object every render, so consumers re-render even when neither `user` nor `theme` actually changed — the fix is to memoize the value object with `useMemo`.
Splitting contexts and memoizing consumers
Splitting one large context into several narrower ones (e.g. separate `UserContext` and `ThemeContext` instead of one combined `AppContext`) means a component only re-renders when the SPECIFIC context it consumes changes. Wrapping expensive consumer subtrees in `React.memo` prevents re-rendering when the PARENT re-renders for unrelated reasons, though it does not prevent re-renders caused directly by a consumed context changing.
React DevTools Profiler is the authoritative tool here: record an interaction, then read the flame graph — each bar's width is render duration, and hovering shows "why did this render" (props changed, state changed, context changed, or parent re-rendered). Never guess at re-render causes; profile first, and always change one lever at a time so you can attribute the improvement correctly.
See It
Visualizations
Visualization
One context change re-renders every consumer
Even <ThemeToggle/> re-renders when `user` changes, because value is one combined object.
Build It
Code Examples
The unstable-value bug, and the useMemo fix
function AppProviderBad({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
// BUG: a new object every render -> every consumer re-renders every time
// AppProviderBad itself re-renders, for ANY reason.
return (
<AppContext.Provider value={{ user, theme, setUser, setTheme }}>
{children}
</AppContext.Provider>
);
}
function AppProviderFixed({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
// FIX: stable reference unless user/theme actually changed
const value = useMemo(
() => ({ user, theme, setUser, setTheme }),
[user, theme]
);
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}Splitting one context into two narrower ones
const UserContext = createContext(null);
const ThemeContext = createContext('light');
function AppProviders({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const userValue = useMemo(() => ({ user, setUser }), [user]);
const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<UserContext.Provider value={userValue}>
<ThemeContext.Provider value={themeValue}>
{children}
</ThemeContext.Provider>
</UserContext.Provider>
);
}
// <ThemeToggle/> now only ever re-renders when theme actually changes
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>{theme}</button>;
}Remember
Key Takeaways
- Any Context value change re-renders EVERY consumer of that context, regardless of which field they actually use.
- An inline object/array/function literal as a Provider value is a NEW reference every render — always useMemo it.
- Splitting a large context into several narrow ones limits the blast radius of a change to only relevant consumers.
- React.memo stops re-renders from an unrelated parent re-render, but not from the consumed context itself changing.
- Always profile with React DevTools before optimizing — "why did this render" tells you the actual cause, not a guess.
Do It
Practice
- 1Build a two-context app (User + Theme) with a render-count badge on each consumer; toggle theme and confirm User consumers do not re-render.
- 2Record a React DevTools Profiler session on a laggy list and identify the exact component causing the widest flame graph bar.
- 3Take one combined context with an inline value object, reproduce the wasted re-renders, then fix it with useMemo and re-profile to confirm the fix worked.