Why Your React App Re-renders Too Much (And How to Fix It)
React is famous for being fast, largely due to its Virtual DOM. However, "fast" doesn't mean "magic." One of the most common performance bottlenecks in React applications is unnecessary re-rendering.
When a component re-renders, React executes the function again, recalculates the Virtual DOM, and compares it to the previous version. If this happens too often or on heavy components, your UI can become laggy and unresponsive.
Here is a guide to understanding why this happens and practical strategies to fix it.
1. Understanding the Render Cycle
First, it is vital to understand when React renders. By default, a component re-renders if:
- Its State Changes: A call to
useStateoruseReducer. - Its Parent Re-renders: If a parent re-renders, all its children re-render by default, regardless of whether their props changed.
- Context Changes: Any component consuming a Context will re-render when that Context’s value updates.
The most common misconception is that a child component only re-renders if its props change. This is false. Unless you explicitly tell React otherwise, a child always re-renders when the parent does.
2. The Culprit: Parent-Induced Re-renders
The Problem
Imagine you have a Parent component with a simple counter and a heavy Child component.
const Parent = () => {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
{/* The Child re-renders every time 'count' changes, even though it doesn't use 'count'! */}
<HeavyChild />
</div>
);
};
Every time you click the button, Parent re-renders. Consequently, HeavyChild also re-renders, wasting resources.
The Fix: React.memo
You can wrap the child component in React.memo. This tells React: "Only re-render this component if its props have actually changed."
const HeavyChild = React.memo(() => {
console.log("Child rendered");
return <div className="heavy">I am a heavy component</div>;
});
Now, clicking the button in Parent updates the state, but HeavyChild stays dormant because it received no new props.
3. The Trap: Referential Equality
The Problem
You successfully added React.memo, but your component still keeps re-rendering. Why? This usually happens when you pass objects or functions as props.
In JavaScript, {} !== {} and function() {} !== function() {}. Every time a component renders, all functions and objects defined inside it are recreated with new references in memory.
const Parent = () => {
const [darkTheme, setDarkTheme] = useState(false);
// This function is RE-CREATED on every render
const handleClick = () => {
console.log("Clicked");
};
return (
<div className={darkTheme ? 'dark' : 'light'}>
<button onClick={() => setDarkTheme(!darkTheme)}>Toggle Theme</button>
{/* Even with React.memo, Child re-renders because 'onClick' is a NEW function reference */}
<MemoizedChild onClick={handleClick} />
</div>
);
};
From React's perspective, the onClick prop changed, so React.memo allows the re-render.
The Fix: useCallback and useMemo
To preserve the "reference" of a function or object across renders, use hooks.
useCallback: Memoizes a function definition.useMemo: Memoizes a calculated value (object, array, or heavy calculation).
const Parent = () => {
const [darkTheme, setDarkTheme] = useState(false);
// React keeps the same function reference across renders
const handleClick = useCallback(() => {
console.log("Clicked");
}, []); // Dependency array is empty, so it never changes
return (
<div className={darkTheme ? 'dark' : 'light'}>
<button onClick={() => setDarkTheme(!darkTheme)}>Toggle Theme</button>
{/* Now React.memo works because 'onClick' is referentially equal */}
<MemoizedChild onClick={handleClick} />
</div>
);
};
4. The Context API Pitfall
The Problem
React Context is great for global state, but it has a major performance "gotcha." If you store a complex object in a Context Provider, every single consumer of that context will re-render whenever any part of that object changes.
If your Context value looks like this: {{ user, theme, settings }}, and you update theme, components that only care about user will typically still force a re-render check.
The Fix: Split Contexts or Memoize Value
- Split Contexts: Instead of one giant
AppContext, useUserContextandThemeContext. - Memoize the Provider Value:
const MyProvider = ({ children }) => {
const [user, setUser] = useState(null);
// Without useMemo, this object is recreated every render, forcing consumers to update
const value = useMemo(() => ({ user, setUser }), [user]);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};
5. How to Spot Re-renders
Don't guess—measure.
- React DevTools Extension: Install this in Chrome/Firefox.
- Profiler Tab: Record a session while using your app. It will show you a "Flamegraph" of what rendered and how long it took.
- "Highlight updates when components render": Turn this setting on in React DevTools. It puts a green/yellow outline around components in your browser whenever they re-render.
Comments
Sign in to join the conversation