How React Actually Renders (and Why Your App Re-renders Too Much)
I spent two days last week staring at the React Profiler, wondering why a simple filter dropdown was causing 47 components to re-render. Forty-seven. For a dropdown.
That's when I stopped just using React and started actually understanding how it works under the hood. Turns out, a lot of the "mysterious" performance issues make complete sense once you get the rendering model. Let's break it down.
First — what even is a "render"?
In React, a render is just React calling your component function. That's it. When React renders <UserCard />, it literally runs the UserCard function and looks at what JSX came out.
People think rendering = DOM update. It doesn't. Rendering is React running your function. Updating the DOM is a separate step that only happens if something actually changed.
This distinction matters a lot.
What triggers a re-render?
Three things cause React to re-render a component:
State changes — you call setState or dispatch to a reducer. React re-renders that component and everything below it in the tree.
Props changes — when a parent re-renders, all its children re-render too. Even if the child's props didn't change. Yes, really.
Context changes — if a useContext value updates, every component consuming that context re-renders. The diagram above shows the full flow. Notice there's no "props actually changed" check by default — React just re-renders the whole subtree. This is where most performance issues come from.
The render, reconcile, commit cycle
Once React decides to render, here's what happens:
Render phase — React calls your component function and builds a new virtual DOM tree. This is pure JavaScript, nothing touches the browser yet.
Reconciliation — React compares the new virtual DOM with the previous one. This is the "diffing" step. React figures out the minimum set of changes needed.
Commit phase — React applies those changes to the actual DOM. This is the expensive step, but React tries to minimize it.
The key insight: just because React renders your component doesn't mean it updates the DOM. If nothing visually changed, the commit phase does nothing. Renders are cheap. DOM updates are expensive.
The re-render problem in real life
Here's a pattern I see everywhere — and had in my own code:
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Click me</button>
<ExpensiveChild /> {/* re-renders every time count changes */}
</div>
);
}
Every time you click the button, ExpensiveChild re-renders — even though it has nothing to do with count. React doesn't know that. It just sees "parent rendered, re-render all children."
Fix 1 — React.memo
Wrap the child in React.memo and React will skip re-rendering it if its props haven't changed:
tsx
const ExpensiveChild = React.memo(() => {
return <div>I only render when my props change</div>;
});
Now clicking the button won't touch ExpensiveChild at all. But there's a catch — if you're passing a function or object as a prop, this breaks down fast.
Fix 2 — useCallback for stable function references
tsx
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // stable reference across renders
return <ExpensiveChild onClick={handleClick} />;
}
Without useCallback, handleClick is a brand new function object on every render. React.memo sees a new prop value and re-renders anyway. useCallback gives you the same function reference between renders so the memo check actually works.
Fix 3 — useMemo for expensive calculations
tsx
const filteredList = useMemo(() => {
return hugeList.filter(item => item.active);
}, [hugeList]);
Don't run this on every render. Memoize it and only recalculate when hugeList actually changes.
When NOT to memoize
This is the part nobody talks about — memoization has a cost too.
React has to store the previous value, run the comparison, and decide whether to use the cached result. For simple components and cheap calculations, this overhead is often more expensive than just re-rendering.
My rule of thumb:
React.memo→ only for components that render often AND are expensive to renderuseMemo→ only for genuinely expensive calculations (think filtering/sorting 1000+ items)useCallback→ mostly when passing callbacks to memoized children or asuseEffectdeps
Don't sprinkle these everywhere "just in case." Profile first, optimize second.
How to actually find the problem — React Profiler
Stop guessing. Open React DevTools, go to the Profiler tab, hit record, interact with your app, and stop recording.
You'll see a flamegraph showing exactly which components rendered and how long each one took. The bars that are wide and re-render repeatedly are your targets.
I found my 47-component re-render problem in about 3 minutes with the Profiler. Turned out a context value was being recreated on every render because the object wasn't memoized. One useMemo fixed it.
The mental model to remember
React rendering is a pull system, not a push system. When state changes, React doesn't surgically update just the affected component — it re-runs the entire subtree from that point downward, then figures out what actually needs to change in the DOM.
Your job as a developer is to give React hints — via memo, useCallback, useMemo — about what hasn't changed so it can skip work.
But don't optimize blindly. Profile first, find the actual bottleneck, then apply the right fix.
What's next
Day 3 is going to be on component design patterns — specifically compound components, render props, and custom hooks, and when to reach for each one. These patterns come up a lot in system design interviews so worth understanding properly.
Drop a comment if the Profiler thing clicked for you — or if you've had a wild re-render bug I should know about.
