React Context Performance

Eliminate wasteful consumer re-renders by splitting context values, memoizing providers, and using selector patterns.

TL;DR

  1. Prevent cascading consumer re-renders by splitting combined context objects.
  2. Separate volatile data state from invariant dispatch action functions.
  3. Memoize Provider value props with useMemo to preserve referential equality.

Value Object Memoization

    Unmemoized Provider Bug

    Creating new object in provider value triggers renders on every tick.

    // BAD: New object on every render
    <Context.Provider value={{ count, inc }}>
    Memoized Provider Value

    Wrap value in useMemo so consumers only re-render on changes.

    const value = useMemo(() => ({
      count,
      inc,
    }), [count, inc]);
    return (
      <Context.Provider value={value}>
        {children}
      </Context.Provider>
    );
    Callback Stabilization

    Wrap updater methods in useCallback before passing to context.

    const inc = useCallback(() => {
      setCount((c) => c + 1);
    }, []);

Context Splitting Pattern

    Split State and Dispatch

    Dispatch context remains completely stable across all state changes.

    const StateCtx = createContext(initial);
    const DispatchCtx = createContext(noop);
    function Provider({ children }) {
      const [state, dispatch] =
        useReducer(reducer, initial);
      return (
        <DispatchCtx.Provider value={dispatch}>
          <StateCtx.Provider value={state}>
            {children}
          </StateCtx.Provider>
        </DispatchCtx.Provider>
      );
    }
    Action-Only Consumer

    Component consuming only dispatch never re-renders on state.

    export function AddButton() {
      const dispatch = useContext(DispatchCtx);
      const add = () => dispatch({ type: 'add' });
      return <Button onClick={add}>Add</Button>;
    }
    Data-Only Consumer

    Component reads state without subscribing to action triggers.

    export function CounterBadge() {
      const state = useContext(StateCtx);
      return <Badge text={state.count} />;
    }

Domain Segregation

    Frequency Separation

    Isolate rapid updates from static configuration values.

    // Fast changes: separate context
    const MousePositionCtx = createContext({ x: 0, y: 0 });
    // Slow changes: separate context
    const UserProfileCtx = createContext(user);
    Subtree Localization

    Mount providers as close to consumers as possible.

    // Mount provider only around the relevant table widget
    <TableProvider>
      <DataTable />
    </TableProvider>
    Intermediary Memo Component

    Extract children into memoized subcomponents below context.

    function Parent() {
      const data = useContext(MyCtx);
      return <MemoChild data={data.specificField} />;
    }

High-Frequency Alternatives

    useSyncExternalStore

    Subscribe to external store with fine-grained selectors.

    import { useSyncExternalStore } from 'react';
    const val = useSyncExternalStore(
      store.sub,
      store.getSnapshot
    );
    External Store Libraries

    Zustand provides automatic selector-based subscriptions.

    const count = useStore((state) => state.count);
    Context Evaluation Rule

    Use Context for theme and auth; use stores for large data.

    // Theme, Auth, Config -> Context
    // Tables, Carts, Streams -> External Stores

Tips

  1. Split frequently changing state from rarely changing state into separate context providers to isolate re-render cascades.
  2. Always wrap context provider value objects in useMemo so consumers only re-render when properties actually change.

Warnings

  1. React Context is designed for low-frequency updates; high-frequency data like animation coordinates will degrade performance.
  2. Remember that React.memo cannot stop a component from re-rendering if that component calls useContext on an updated context.

In Practice

FAQ