When Should You Not Use React.memo? 7 Performance Anti-Patterns
Avoid React.memo when components receive unstable props, render cheaply, or consume frequently updating context, as the O(n) shallow comparison overhead in packages/react/src/ReactMemo.js often exceeds the cost of re-rendering.
React.memo is a higher-order component in the facebook/react repository that caches function component output using shallow prop comparison. While it prevents unnecessary reconciliation, applying it indiscriminately introduces comparison costs that degrade performance when props change frequently or component renders are trivial. Understanding exactly when not to use React.memo requires analyzing its implementation, which relies on Object.is semantics and executes on every parent render regardless of child purity.
How React.memo Works Under the Hood
Before identifying anti-patterns, examine the mechanism implemented in packages/react/src/ReactMemo.js. The wrapper stores previous props and executes a shallow comparison using Object.is for each property every time the parent re-renders. This check runs in O(n) time relative to the number of props, then either returns the cached result or invokes the original component function, meaning the comparison cost is paid regardless of whether the child updates.
Situations Where You Should Not Use React.memo
Unstable Props from Inline Objects or Functions
When parents pass inline objects or arrow functions, React.memo never skips re-renders yet still pays the comparison cost. In packages/react/src/ReactMemo.js, the shallow equality check compares object references, not contents, so {data: 1} created inline fails the check every time.
// Parent creates new references each render
function Parent() {
const [count, setCount] = React.useState(0);
return (
<Child
config={{ mode: 'dark' }} // New object every time
onClick={() => setCount(c => c + 1)} // New function every time
/>
);
}
const Child = React.memo(function Child({config, onClick}) {
console.log('This logs every time despite memo');
return <button onClick={onClick}>Count</button>;
});
Why it’s bad: config is a new object each time, so the shallow check always fails. Child re-renders on every Parent update, and the memo wrapper adds an unnecessary O(n) comparison.
Components with Cheap Render Costs
If a component renders minimal JSX (a single div or text node), the comparison overhead dominates. The Object.is check in React.memo requires iterating all props, which often costs more CPU cycles than the component's VDOM creation and diffing.
Heavy Context or State Dependencies
React.memo only inspects props, not context or internal state. If a component consumes a frequently updating context via useContext or uses useReducer with high-frequency dispatches, the component re-renders regardless of memoization. The wrapper adds comparison work without preventing updates triggered by packages/react/src/ReactComponentTreeHook.js.
Nested Memoization Wrappers
Wrapping a component already processed by React.forwardRef or another React.memo layer yields no benefit. As seen in packages/react/src/ReactForwardRef.js, these wrappers chain internally, and nesting them obscures debugging without improving performance.
Animation and High-Frequency Updates
Animation loops using requestAnimationFrame often trigger updates every 16ms. Adding React.memo increases per-frame CPU usage with constant shallow comparisons, potentially causing jank rather than preventing it.
Custom Deep Equality Comparators
Providing a custom areEqual function to React.memo replaces the default shallow check:
function deepEqual(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
}
const DeepChild = React.memo(function({complexData}) {
console.log('Deep comparison runs first');
return <div>{complexData.message}</div>;
}, deepEqual);
When to use: Only if the component render is extremely expensive and the cost of JSON.stringify is lower than recomputing the UI. In most cases, refactor the data shape instead of using deep equality.
Debugging and Code Readiability Trade-offs
Excessive memoization spreads performance intent across multiple files, complicating component tracing in packages/react/src/ReactComponentTreeHook.js. When performance gains are negligible, the debugging complexity outweighs benefits, making the component harder to reason about during testing.
Proper Implementation Patterns
Stable Props with Expensive Children
Use useMemo to maintain referential equality when passing objects to memoized children that perform heavy rendering:
function Parent() {
// Stable references prevent unnecessary comparison failures
const config = React.useMemo(() => ({ mode: 'dark' }), []);
const items = React.useMemo(() => generateHugeArray(), []);
return <ExpensiveList config={config} items={items} />;
}
const ExpensiveList = React.memo(function({config, items}) {
// This only runs when items actually change
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
});
Why it’s good: config and items are stable, and rendering the list is costly. React.memo prevents re-rendering when the parent updates unrelated state.
Summary
- Do not use React.memo when parents pass inline objects or functions that recreate references every render, as the shallow comparison in
packages/react/src/ReactMemo.jsalways fails. - Avoid memoization for components with trivial render costs where O(n) prop comparisons exceed rendering time.
- Skip React.memo when components consume frequently changing context or state, since these bypass prop comparison entirely.
- Never nest
React.memoinsideReact.forwardRefor other memoization layers; consultpackages/react/src/ReactForwardRef.jsfor proper composition patterns. - Reserve custom comparators for extreme cases where deep equality is cheaper than re-rendering, as implemented in
packages/react/src/ReactMemo.js.
Frequently Asked Questions
Does React.memo perform deep or shallow comparison?
React.memo performs a shallow comparison using Object.is semantics. According to packages/react/src/ReactMemo.js, it compares property references, not object contents. For deep structures, you must provide a custom comparator function, though this is often slower than the re-render itself.
Can React.memo prevent re-renders caused by context or state changes?
No. React.memo only compares props. If a component uses useContext or internal useState/useReducer hooks that update, the component re-renders regardless of memoization. The wrapper has no visibility into context dependencies or internal state transitions.
Should I wrap every component in React.memo by default?
Absolutely not. Defensive memoization adds unnecessary CPU overhead from prop comparisons on every parent render. Only apply React.memo to expensive components that receive stable props, as verified by profiling. Unnecessary wrappers also complicate debugging traces in packages/react/src/ReactComponentTreeHook.js.
How does React.memo interact with React.forwardRef?
These wrappers compose normally, but nesting them provides no additional performance benefit. The source in packages/react/src/ReactForwardRef.js shows how refs propagate through memoized components; use React.memo(React.forwardRef(Component)) only when both optimizations are independently justified, not as a default pattern.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s https://instagit.com/install.md