You update one small piece of state:
setCount(count + 1);
And suddenly your console looks like this:
Header rendered
Sidebar rendered
ProductList rendered
ProductCard rendered
Footer rendered
Your first thought might be:
"Why did half of my React app render again? Isn't this bad for performance?"
Not necessarily.
One of the biggest misconceptions about React performance is that every re-render is a problem.
It isn't.
React components are designed to render when their inputs change. A render is simply part of how React determines what the UI should look like.
The real question is not:
"How do I stop React from re-rendering?"
It's:
"Is this re-render causing unnecessary expensive work?"
That distinction is the foundation of understanding React performance.
What Does a Re-render Actually Mean?
Consider this simple component:
function Counter() {
const [count, setCount] = useState(0);
console.log("Counter rendered");
return (
<>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</>
);
}
When the button is clicked, count changes.
React then needs to calculate what the component should produce with the new state.
A simplified mental model is:
State changes
↓
Component renders
↓
Next UI is calculated
↓
React reconciles the result
↓
Necessary changes are committed
The important distinction is:
Re-render ≠ Complete DOM rebuild
A re-render means React performs rendering work again to determine the component's next output.
It does not mean React destroys the entire browser DOM and recreates it from scratch.
In fact, React can render a component and discover that little—or even nothing—needs to change in the actual DOM.
So:
A re-render is normal. Unnecessary expensive work is what you should care about.
What Causes a Component to Render?
Before optimizing a component, you need to understand why it rendered in the first place.
There are several common causes.
1. Its State Changes
This is the most obvious case.
const [count, setCount] = useState(0);
When you call:
setCount(count + 1);
React receives a state update and schedules work to update the component.
The component needs to calculate its UI using the new state.
That's completely normal.
2. Its Parent Renders
This is where many developers get confused.
Consider:
function App() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>
{count}
</button>
<Header />
</>
);
}
Now imagine:
function Header() {
console.log("Header rendered");
return <h1>My Website</h1>;
}
When count changes, App renders again.
As part of processing the updated tree, React may also render Header.
So you might see:
Header rendered
even though Header doesn't use count.
This often leads developers to think:
"React is unnecessarily rendering my entire application!"
But that's too simplistic.
If Header is tiny and its render is cheap, another render may have practically no meaningful performance impact.
The important question is not whether Header rendered.
It's:
How much work did that render actually do?
3. Its Props Change
Props are another source of updates.
function Profile({ name }) {
return <h1>{name}</h1>;
}
Used like this:
function App() {
const [name, setName] = useState("Nayan");
return <Profile name={name} />;
}
When name changes, Profile receives different input and needs to calculate its new UI.
That's exactly what we expect.
New props
↓
New component input
↓
New render
Again, this isn't automatically a performance problem.
4. A Context Value Changes
Context can also cause components that consume that context to update.
For example:
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}
A component can consume it:
function Navbar() {
const theme = useContext(ThemeContext);
return <nav className={theme}>Navbar</nav>;
}
When the context value changes, components consuming that context can update.
This becomes especially important when one context contains many unrelated pieces of frequently changing state.
For example:
User
Theme
Notifications
Cart
Language
If everything lives inside one large context, a change to one part can cause more consumers to be affected than necessary.
Sometimes separating unrelated concerns into smaller contexts is a better design.
5. State Used by a Custom Hook Changes
Custom hooks don't render independently.
Consider:
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
return width;
}
And:
function App() {
const width = useWindowWidth();
return <h1>{width}px</h1>;
}
If the hook's state changes, the component using that hook is updated.
A useful rule is:
Hooks don't render independently. The component using them renders.
This distinction becomes helpful when debugging applications with many custom hooks.
The Biggest Source of Unnecessary Work: State in the Wrong Place
Now we get to one of the most useful React performance ideas.
Consider this:
function App() {
const [search, setSearch] = useState("");
return (
<>
<SearchBox
search={search}
setSearch={setSearch}
/>
<ExpensiveDashboard />
<Sidebar />
<Footer />
</>
);
}
Imagine the user types:
N
Na
Nay
Naya
Every keystroke changes search.
Because search lives in App, App updates on every keystroke.
That means React has to process the tree below that component repeatedly.
Now ask:
Does
searchactually need to live inApp?
If only SearchBox needs that state, placing it closer to SearchBox can reduce the scope of the updates.
For example:
App
├── SearchBox
│ └── Search state
├── ExpensiveDashboard
├── Sidebar
└── Footer
Instead of:
App
├── Search state
├── SearchBox
├── ExpensiveDashboard
├── Sidebar
└── Footer
This idea is called state colocation.
And it is one of the most useful optimization techniques in React because you're improving the structure of the application rather than adding optimization APIs everywhere.
Before reaching for:
React.memo(...)
or:
useMemo(...)
or:
useCallback(...)
ask:
Can this state live closer to where it is actually needed?
Often, that's the better solution.
Why "Re-render" Doesn't Mean "DOM Update"
This distinction is worth making extremely clear.
Suppose React renders:
<h1>Welcome</h1>
and the next render produces:
<h1>Welcome</h1>
The component rendered.
But the resulting UI is effectively the same.
So React may have no meaningful DOM change to commit.
That's why these concepts should stay separate:
Render
↓
Reconciliation
↓
Commit
↓
DOM changes
A render can happen without a meaningful DOM update.
This is why seeing:
Component rendered
in the console is not enough evidence that something is wrong.
What Is Reconciliation?
During an update, React needs to determine how the new element tree relates to the previous one.
Imagine:
Previous UI
Count: 0
becoming:
Next UI
Count: 1
The surrounding structure may remain the same while the displayed value changes.
React's reconciliation process determines how the current tree should transition toward the next one.
You may have heard this explained as:
"React compares the old Virtual DOM with the new Virtual DOM."
That's a useful beginner-friendly explanation, but it's an oversimplification.
React has its own internal representations and reconciliation machinery. It isn't simply taking two snapshots of the browser DOM and comparing them.
For practical React development, the more useful mental model is:
React calculates the next UI and determines what work is required to transition the current tree toward it.
What Happens During the Commit?
After React finishes the necessary rendering and reconciliation work, it reaches the commit phase.
This is where React applies the required changes to the host environment.
In a browser, that means updating the DOM.
For our counter:
Count: 0
eventually becomes:
Count: 1
The key point is that React doesn't need to rebuild the entire DOM simply because a component rendered again.
It commits the changes that are actually required.
What About the Browser?
React isn't the final step.
After the DOM has been updated, the browser still needs to turn that state into pixels.
A simplified browser pipeline looks like:
DOM + CSS
↓
Style
↓
Layout
↓
Paint
↓
Composite
↓
Pixels
This is important because React performance and browser rendering performance aren't exactly the same thing.
You can have:
- expensive React rendering
- expensive DOM work
- expensive layout
- expensive painting
Understanding where the actual cost occurs is much more useful than simply counting renders.
Why Can Re-renders Be Expensive?
A re-render itself isn't necessarily expensive.
The work performed during that render might be.
Imagine a component that:
- filters thousands of records
- renders a huge list
- performs complex calculations
- renders a complicated chart
- causes many expensive children to update
Now frequent renders may matter.
Compare:
Render
↓
Small component
↓
Cheap work
↓
Probably fine
with:
Render
↓
Heavy calculations
↓
Large list
↓
Expensive children
↓
Noticeable lag
That's the difference.
So don't optimize because you see the word:
rendered
in your console.
Optimize when there is unnecessary work that actually matters.
What About React.memo?
React.memo can prevent a child component from rendering when its props haven't changed, assuming the relevant comparison indicates that the props are equal.
For example:
const Profile = React.memo(function Profile({ name }) {
console.log("Profile rendered");
return <h1>{name}</h1>;
});
Now:
function App() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>
{count}
</button>
<Profile name="Nayan" />
</>
);
}
When count changes, App renders again.
But Profile still receives the same name.
Because it's memoized, React can potentially skip rendering Profile.
Conceptually:
Parent renders
↓
Child props unchanged
↓
Memoization can skip child render
But React.memo isn't something you should automatically add to every component.
It is most useful when:
- the parent renders frequently
- the child is relatively expensive
- the child's props often remain unchanged
- avoiding the render actually provides a measurable benefit
For a tiny component like:
function Label({ text }) {
return <span>{text}</span>;
}
memoization may provide little practical benefit.
Don't use
React.memobecause a component can re-render. Use it when skipping that render is actually useful.
The Hidden Problem: New Object References
There is another reason memoization can sometimes fail to provide the expected benefit.
Consider:
const Profile = React.memo(function Profile({ user }) {
return <h1>{user.name}</h1>;
});
And:
function App() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>
{count}
</button>
<Profile user={{ name: "Nayan" }} />
</>
);
}
Every time App renders, this creates a new object:
{ name: "Nayan" }
Even though the contents are the same, the object itself is a new reference.
Conceptually:
Previous render → Object A
Next render → Object B
Object A !== Object B
This matters because memoization compares props using shallow equality.
The same idea can apply to arrays and functions.
For example:
<Profile items={["React", "Next.js"]} />
or:
function App() {
const handleClick = () => {
console.log("Clicked");
};
return <Child onClick={handleClick} />;
}
A new function is created during each render.
But don't take this to mean:
"New objects and functions are bad."
They're not.
Creating a new object or function is perfectly normal JavaScript.
It only becomes relevant when reference stability is important for a specific optimization.
So What Is useCallback For?
useCallback can preserve a function reference between renders when its dependencies haven't changed.
For example:
const handleClick = useCallback(() => {
console.log("Clicked");
}, []);
This can be useful when the callback is passed to a memoized child:
const Child = React.memo(function Child({ onClick }) {
return <button onClick={onClick}>Click me</button>;
});
Without stable references, a newly created callback can cause the child's props to appear changed.
But that doesn't mean every function needs useCallback.
This:
const handleClick = () => {
console.log("Clicked");
};
is perfectly fine in many situations.
The better rule is:
Use
useCallbackwhen a stable function reference enables a meaningful optimization.
Not simply because a function is created during rendering.
And What About useMemo?
useMemo can help avoid repeating an expensive calculation when its dependencies haven't changed.
For example:
const filteredProducts = useMemo(() => {
return products.filter((product) => product.inStock);
}, [products]);
This can make sense when the calculation is genuinely expensive.
It can also be useful when you need a stable value reference for another optimization.
But this:
const name = useMemo(() => "Nayan", []);
usually adds complexity without providing a meaningful benefit.
So again:
useMemois an optimization tool, not a default requirement.
The Most Common React Performance Mistake
A developer opens the console and sees:
Dashboard rendered
Dashboard rendered
Dashboard rendered
Dashboard rendered
They immediately add:
React.memo
useMemo
useCallback
everywhere.
But they haven't answered the most important question:
Is the render actually causing a performance problem?
A small component can render many times and still be completely fine.
Meanwhile, a large component doing expensive work may be worth optimizing even if it renders less frequently.
So don't optimize based on the number of renders alone.
Optimize based on the cost of the work.
How to Investigate a Re-render
When you notice frequent rendering, work through these questions.
1. What caused it?
Was it:
- state?
- props?
- context?
- a parent update?
- state used by a custom hook?
- another update source?
First understand the trigger.
2. Does the component actually need that state?
If frequently changing state is stored high in the tree, ask whether it can move closer to the components that actually use it.
State colocation is often a cleaner solution than memoization.
3. Is the render expensive?
Look for:
- large lists
- heavy calculations
- complex charts
- expensive child components
- slow interactions
- visible UI lag
If the render is cheap, there may be nothing to fix.
4. Can the component structure be improved?
Sometimes the best optimization isn't an optimization API.
It might simply be:
Better component boundaries
Better state placement
Smaller update scope
Good architecture can prevent unnecessary work naturally.
5. Would memoization actually help?
Only after understanding the problem should you consider:
React.memo
useMemo
useCallback
And ideally, measure whether the change actually improved the situation.
The React Performance Mindset
This is the mindset I want you to remember.
When you see:
Component rendered
don't immediately think:
"Something is wrong."
Instead ask:
Why did it render?
Then:
What work did the render perform?
Then:
Is that work expensive enough to matter?
And finally:
What's the simplest way to reduce that work?
That might be:
Move state
↓
Split components
↓
Reduce unnecessary work
↓
Memoize when useful
Not:
Something rendered
↓
useMemo
↓
useCallback
↓
React.memo
↓
Everything is memoized
The Mental Model to Remember
React performance becomes much easier to understand when you separate these concepts:
State / Props / Context change
↓
React renders
↓
Next UI is calculated
↓
Reconciliation
↓
Commit
↓
DOM is updated
↓
Browser renders pixels
But don't confuse:
Re-render
≠
DOM update
or:
Parent re-render
≠
Performance problem
or:
Memoization
≠
Always necessary
Final Takeaway
React components are supposed to render.
A state change, a parent update, changed props, or a context update can cause rendering work.
That alone isn't a problem.
The real problem is unnecessary expensive work.
So when you see a component rendering more often than expected, don't immediately reach for React.memo, useMemo, or useCallback.
Instead:
Notice the render
↓
Understand what triggered it
↓
Measure the actual cost
↓
Improve component boundaries
↓
Keep state close to where it's needed
↓
Reduce unnecessary work
↓
Use memoization when it provides real value
The goal isn't to build an application where nothing re-renders.
The goal is to build an application where the work that happens during updates is appropriate for what actually changed.
So don't ask:
"How do I stop React from re-rendering?"
Ask:
"Is this render doing unnecessary work, and what's the simplest way to fix it?"
That small change in mindset is one of the most important steps toward understanding React performance.
And once you understand why a component rendered, what work it performed, and what actually reached the DOM, React optimization stops being a collection of tricks and starts becoming a reasoning problem.
Top comments (0)