React Performance: Stop Using useMemo For Everything
Comments
Sign in to join the conversation
Sign in to join the conversation
In the React ecosystem, useMemo and useCallback are often treated as magic wands. "Is the app slow? Wrap everything in memo!"
The specific advice has shifted over the years (especially with the advent of the React Compiler in late 2024), but the core principle remains: Memoization has a cost. It costs memory to store the values, and it costs CPU to compare the dependencies.
Here is a deep dive into real React performance optimization, beyond just wrapping functions.
useMemoEvery time you write:
const value = useMemo(() => compute(a, b), [a, b]);
React has to:
[a, b].If compute(a, b) is just a + b, the comparison is more expensive than the calculation.
Rule of Thumb:
Only use useMemo if the calculation involves iterating over thousands of items or is a referential dependency for a child component (e.g., passing an object to a useEffect or a memoized child).
The #1 cause of unnecessary re-renders is global state that shouldn't be global.
The Problem:
You have a context provider wrapping your entire app that holds theme and currentUser. A component deep in the tree updates theme. Every single component consuming that context re-renders.
The Fix: Push state down.
If only the Header needs to know about the isMenuOpen state, do not put it in a global store. Put it in the Header.
// Bad
const App = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
return (
<>
<Header isOpen={isMenuOpen} />
<ExpensiveDashboard /> {/* Re-renders when menu opens! */}
</>
);
};
// Good
If you are rendering a list of 5,000 items, no amount of useMemo will save you. The DOM is the bottleneck.
If a user can only see 10 items at a time, only render 10 items.
Libraries like react-window or tanstack-virtual are mandatory for long lists. They calculate which items are currently in the viewport and only insert those specific nodes into the DOM.
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => (
<div style={style}>Row {index}</div>
);
const Example = () => (
<List
height={150}
itemCount
Your users don't need the "Settings" page code when they land on the "Home" page.
Next.js does this automatically for pages, but you can do it for heavy components too.
import dynamic from 'next/dynamic';
// This 5MB charting library won't load until the component is actually rendered
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <p>Loading Chart...</p>,
ssr: false,
});
This drastically reduces the "First Contentful Paint" (FCP) and "Time to Interactive" (TTI).
React is fast enough by default for 99% of interactions. When it does get slow, look for:
Reach for useMemo last, not first.