A Deep Dive into React Performance
In React, a component re-rendering is not inherently bad; it's how the library works. However, *unnecessary* re-renders—when a component's DOM output doesn't change—can add up, leading to sluggish UIs and a poor user experience. Your goal as a developer isn't to stop all re-renders, but to eliminate the wasteful ones. This is where React's optimization tools come in.
Part 1: `React.memo` - Stopping Component Re-renders
`React.memo` is a Higher-Order Component (HOC) that performs a simple function: it **prevents a component from re-rendering if its props haven't changed.**
By default, `React.memo` performs a **shallow comparison** of the props object. This means it checks if `prevProps.foo === nextProps.foo` for every prop. This works great for simple values like strings, numbers, and booleans. It fails, however, for objects, arrays, and functions, which are re-created (and thus have a new reference) on every render.
❌ Problem
function App() {
const [count, setCount] = useState(0);
// This child will re-render
// when 'count' changes.
return <ChildComponent data={...} />;
}✔️ Solution
const MemoizedChild = React.memo(ChildComponent);
function App() {
const [count, setCount] = useState(0);
// This child will NOT re-render.
return <MemoizedChild data={...} />;
}Part 2: `useMemo` - Caching Expensive Calculations
The `useMemo` hook is for a different problem. It doesn't memoize a component; it memoizes a **value**.
Imagine you have a function that sorts a 10,000-item list or performs a complex mathematical calculation. If this function runs inside your component, it will re-run on *every single render*, even if its inputs didn't change. `useMemo` solves this by caching the function's return value and only re-running the function if one of its **dependencies** has changed.
// Before: Runs on every render
const sortedList = expensiveSort(list);
// After: Only runs when 'list' changes
const sortedList = useMemo(() => {
return expensiveSort(list);
}, [list]);Part 3: `useCallback` - The Key to `React.memo`
This is where most developers get stuck. You've wrapped your component in `React.memo`, but it's *still* re-rendering. Why?
The problem is likely a function prop.
const MemoizedChild = React.memo(ChildComponent);
function App() {
// This function is RE-CREATED on every render
const handleClick = () => { ... }
// 'handleClick' is a new prop every time,
// which breaks React.memo!
return <MemoizedChild onClick={handleClick} />;
}`useCallback` solves this by memoizing the **function itself**. It returns the *same function reference* between renders, as long as its dependencies don't change. This satisfies `React.memo`'s shallow comparison.
const MemoizedChild = React.memo(ChildComponent);
function App() {
// This function reference is STABLE
const handleClick = useCallback(() => { ... }, []);
// 'handleClick' is the same prop every time.
// React.memo now works!
return <MemoizedChild onClick={handleClick} />;
}Part 4: Profiling - Don't Guess, Measure!
The most important rule of optimization is: **do not optimize prematurely.** Adding `memo`, `useMemo`, and `useCallback` everywhere adds complexity and consumes memory. It can even make your app slower.
Always use the **React DevTools Profiler** first. It will show you which components are re-rendering and why. Focus your efforts on the components that are demonstrably slow.
Key Takeaway: Profile first. Use `React.memo` to stop component re-renders. If `memo` fails, use `useCallback` for function props and `useMemo` for complex object/array props. Use `useMemo` by itself to cache expensive calculations.