React Performance: A Deep Dive into useMemo and useCallback
React's core mechanic is simple: When state changes, the component re-renders. Usually, this is fast enough. But in complex applications, you might find your UI lagging when typing in an input field or switching tabs.
This usually happens because expensive calculations are running on every keystroke, or child components are re-rendering when they don't need to.
React provides two hooks to solve this: useMemo and useCallback.
Referential Equality
To understand these hooks, you must understand how JavaScript compares objects.
const a = { id: 1 };
const b = { id: 1 };
console.log(a === b); // FALSE
Even though they look the same, they are different places in memory. In React, if you define a function inside a component, that function is recreated every single time the component renders.
If you pass that function to a child component, the child thinks "Oh, I got a new prop!" and re-renders too, even if nothing actually changed.
1. useCallback
useCallback caches a function definition between renders.
The Problem:
const Parent = () => {
const [count, setCount] = useState(0);
// This function is recreated on every click!
const handleClick = () => {
console.log('Clicked');
};
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<ChildComponent onClick={handleClick} />
</div>
);
}
Even though ChildComponent shouldn't care about count, it re-renders because handleClick is technically a "new" function.
The Solution:
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // No dependencies, never recreated
2. useMemo
useMemo caches the result of a calculation.
The Problem:
const Component = ({ data }) => {
// This runs on every render. If data is huge, the app freezes.
const sortedData = data.sort((a, b) => a.value - b.value);
return <List items={sortedData} />;
}
The Solution:
const sortedData = useMemo(() => {
return data.sort((a, b) => a.value - b.value);
}, [data]); // Only run if 'data' changes
The Trap: Premature Optimization
You might think, "I should wrap everything in useMemo!" Don't.
These hooks come with a cost. React has to allocate memory to store the dependencies and compare them. For simple calculations (like a + b) or simple button clicks, standard JavaScript is faster than the overhead of useMemo.
Rules of Thumb
- Use
useMemofor heavy data processing (sorting thousands of rows, complex filtering). - Use
useCallbackwhen passing functions to children that are wrapped inReact.memo. - Use
useCallbackwhen a function is in the dependency array of auseEffect.