Memoization
Performance optimization in React often boils down to one simple goal: Stop doing work that doesn’t need to be done.
React is fast by default, but as your application grows, you might find that components re-render more often than necessary. This is where Memoization comes in.
1. Intuition: The Forgetful Painter
Imagine you hire a painter to paint your living room.
- Without Memoization: Every time you buy a new vase, the painter re-paints the entire room, including the walls, ceiling, and furniture, just to make sure everything matches.
- With Memoization: You tell the painter, “Only paint the vase. The walls haven’t changed.”
In React, the “painter” is the Render Phase. If a parent component updates, React (by default) assumes everything inside it might have changed and asks all children to re-render.
[!IMPORTANT] Memoization is not free. It comes with a cost of memory usage (to store the previous result) and computation (to compare props/dependencies). Only use it when you have identified a performance bottleneck.
2. Hardware Reality: The 16ms Budget
Why do we care about re-renders?
Your browser’s main thread is responsible for running JavaScript, handling user input, and painting pixels to the screen. To achieve a smooth 60 frames per second (FPS), the browser has a budget of approximately 16.6ms per frame.
- 10ms: React Render Phase (Calculating the diff)
- 4ms: Browser Paint (Updating the DOM)
- 2ms: Idle / Overhead
If your component takes 20ms to render, the browser drops a frame. The user perceives this as “lag” or “jank”. Memoization helps you stay within this 16ms budget by skipping the “Render Phase” for components that haven’t changed.
3. The Render Cascade
To understand memoization, you must first understand why React re-renders components.
- State or Props Change: When a component’s state or props change, React schedules a re-render.
- Parent Re-renders: By default, if a parent component re-renders, all of its children re-render, regardless of whether their props changed.
This “cascade” of re-renders is usually fine, but for expensive components, it can cause lag.
React Render Cascade
4. React.memo
React.memo is a Higher-Order Component (HOC) that wraps a functional component. It performs a shallow comparison of the new props vs the old props. If they are the same, it skips the re-render.
const MyComponent = React.memo(function MyComponent(props) {
/* only rerenders if props change */
});
Interactive Demo: The Re-render Flash
In this demo, observe how the “Standard List” re-renders every time you increment the parent’s count, even though its props haven’t changed. Toggle “Enable Memoization” to stop the unnecessary work.
5. useMemo
useMemo is a hook that memoizes a value. It’s useful when you have an expensive calculation that shouldn’t run on every render.
const expensiveValue = useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]);
When to use useMemo?
- Expensive Calculations: Filtering a large array, transforming data, or complex math.
- Referential Equality: When you need to pass an object or array to a child component wrapped in
React.memo. If you create the object inline, it will be a new reference every render, breakingReact.memo.
6. useCallback
useCallback is similar to useMemo, but it memoizes a function definition.
const handleClick = useCallback(() => {
console.log('Clicked!', count);
}, [count]);
[!WARNING] Common Pitfall: Using
useCallbackdoes not prevent the function from being created. It prevents the reference from changing. The function inside is still allocated in memory every render, but React discards the new one and returns the old one if deps haven’t changed.
7. The Referential Equality Trap
This is the most common reason memoization fails. In JavaScript, two objects or functions are only equal if they share the same reference.
console.log({} === {}); // false
console.log([] === []); // false
console.log(() => {} === () => {}); // false
Why it matters in React
If you define a function or object inside your component body, it gets recreated on every render.
function Parent() {
// ❌ This creates a NEW function reference every render
const handleItemClick = (id) => console.log(id);
return (
// ❌ Child re-renders because props.onClick changed!
// React.memo won't help here.
<MemoizedChild onClick={handleItemClick} />
);
}
The Fix:
function Parent() {
// ✅ Function reference stays the same unless deps change
const handleItemClick = useCallback((id) => console.log(id), []);
return (
// ✅ Child sees the same prop reference -> Skips render
<MemoizedChild onClick={handleItemClick} />
);
}
8. Summary Checklist
| Tool | Memoizes | Use Case |
|---|---|---|
| React.memo | Component | Skip re-rendering a child if props haven’t changed. |
| useMemo | Value | Skip expensive calculations or stabilize object references. |
| useCallback | Function | Stabilize function references to pass to memoized children. |