HomeAbout Me

React Performance: Optimize Rendering

By Daniel Nguyen
Published in React JS
July 16, 2025
2 min read
React Performance: Optimize Rendering

🧠 Optimize React Rendering Like a Pro

Eliminate unnecessary re-renders and make your UI fly!

React is fast… until it isn’t. While React’s virtual DOM and reconciliation algorithms are designed for efficiency, certain patterns in your app can lead to excessive, unnecessary re-renders that hurt performance—especially on slower devices.

In this guide, you’ll learn:

  • How React’s rendering lifecycle works
  • What causes unnecessary re-renders
  • How to fix them with memo, custom comparators, and props restructuring

Let’s dive deep and make your components render smarter—not harder. 🚀


🌀 The React Rendering Lifecycle

Understanding React’s internal phases helps us understand when and why components re-render:

React Render Cycle

Here’s what happens:

  1. Render Phase React creates virtual DOM elements via React.createElement.

  2. Reconciliation Phase React compares the new elements with the previous render to detect changes.

  3. Commit Phase React updates the actual DOM (if anything changed).

Updating the DOM is expensive, so React avoids it unless necessary. But rendering and reconciling can also become bottlenecks—especially when they happen more often than they need to.


🧾 Why Do React Components Re-render?

A React component will re-render if:

  1. Its props change
  2. Its state changes
  3. It consumes context and the context value changes
  4. Its parent re-renders

If a component re-renders but its output hasn’t changed, that’s an unnecessary re-render.


🛑 Avoiding Unnecessary Re-renders with memo

Let’s take a simple app as an example:

function CountButton({ count, onClick }) {
return <button onClick={onClick}>{count}</button>
}
function NameInput({ name, onNameChange }) {
return (
<label>
Name: <input value={name} onChange={(e) => onNameChange(e.target.value)} />
</label>
)
}
function App() {
const [name, setName] = useState('')
const [count, setCount] = useState(0)
return (
<div>
<CountButton count={count} onClick={() => setCount(c => c + 1)} />
<NameInput name={name} onNameChange={setName} />
{name && <p>{name}'s favorite number is {count}</p>}
</div>
)
}

🔍 The Problem

Every time you click the button, NameInput re-renders—even though its props haven’t changed! Why?

Because the App component re-renders, and React has no way of knowing whether child components need to re-render or not.


✅ The Fix: React.memo

const NameInput = React.memo(function NameInput({ name, onNameChange }) {
return (
<label>
Name: <input value={name} onChange={(e) => onNameChange(e.target.value)} />
</label>
)
})

Now, NameInput will only re-render if name or onNameChange changes.

⚠️ Be careful!

Don’t wrap everything in memo. It adds complexity and might not improve performance if your components are already cheap to render.


🧪 Experiment: What About CountButton?

Try wrapping CountButton in memo:

const CountButton = React.memo(function CountButton({ count, onClick }) {
return <button onClick={onClick}>{count}</button>
})

💥 Surprise: It still re-renders!

Why? Because onClick is a new function instance on every render:

const increment = () => setCount(c => c + 1)

So React sees a prop change and re-renders anyway.


🔁 Fixing That with useCallback

const increment = useCallback(() => setCount(c => c + 1), [])

Now, onClick doesn’t change across renders—and memo works.


⚙️ Custom Comparators with memo

Sometimes, even if a prop object is a new reference, its contents haven’t changed. memo doesn’t know that by default—it just does shallow comparison.

So you can pass a custom comparator:

const Avatar = memo(
function Avatar({ user }: { user: User }) {
return <img src={user.avatarUrl} alt={user.name} />
},
(prevProps, nextProps) => (
prevProps.user.avatarUrl === nextProps.user.avatarUrl &&
prevProps.user.name === nextProps.user.name
)
)

Now the component only re-renders when relevant properties change.


🧼 Use Primitives Instead

If possible, restructure props to avoid custom comparators:

const Avatar = memo(function Avatar({ avatarUrl, name }) {
return <img src={avatarUrl} alt={name} />
})

Now React’s default shallow comparison works perfectly—no need for a custom function.


🧪 Real-World Debugging: React Profiler & Chrome DevTools

To see what’s really going on under the hood:

  1. Use the React DevTools Profiler

    • Record a render
    • Look for unnecessary component renders
    • Enable “Why did this render?” to view the cause
  2. Use Chrome DevTools Performance Tab

    • Simulate 6x CPU throttling
    • Record and analyze a flame graph
    • Find slow re-render paths

🔬 Optimization is only useful if it fixes a real bottleneck.


🧠 Summary

OptimizationUse When…
React.memoChild component receives stable props
useCallbackYou pass a function prop to a memo component
Custom comparatorProps are objects that change by reference only
Primitive propsYou want to avoid writing custom comparators
React ProfilerYou want to confirm actual render performance

🛑 Final Thoughts

Before you reach for memo, fix the slow render first. Memoization only helps avoid work—if the work is still slow, it won’t help.

Check out this article for a deeper look: 👉 Fix the slow render before you fix the re-render

React is powerful, but optimization takes intent. Profile first, measure, then optimize.



Tags

#ReactPerformance

Share

Previous Article
Debounce in JavaScript

Table Of Contents

1
🌀 The React Rendering Lifecycle
2
🧾 Why Do React Components Re-render?
3
🛑 Avoiding Unnecessary Re-renders with memo
4
✅ The Fix: React.memo
5
🧪 Experiment: What About CountButton?
6
🔁 Fixing That with useCallback
7
⚙️ Custom Comparators with memo
8
🧼 Use Primitives Instead
9
🧪 Real-World Debugging: React Profiler & Chrome DevTools
10
🧠 Summary
11
🛑 Final Thoughts

Related Posts

React Performance: Windowing
July 17, 2025
2 min
© 2025, All Rights Reserved.
Powered By

Quick Links

About Me

Legal Stuff

Social Media