RoadmapDay 25 / 80
React (Web)Month 2 · Week 5

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.

Mark this day complete

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.

<AppContext.Provider value={{ user, theme }}>
<Header/> reads user → re-renders (expected)
<ThemeToggle/> reads theme only → re-renders anyway (wasteful)
<Footer/> reads neither → re-renders anyway (wasteful)

Build It

Code Examples

The unstable-value bug, and the useMemo fix

jsx
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

jsx
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

  1. 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.
  2. 2Record a React DevTools Profiler session on a laggy list and identify the exact component causing the widest flame graph bar.
  3. 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.