🎯 React Element Optimization: How to Avoid Unnecessary Renders and Improve Performance
React is an incredibly powerful UI library—but with great power comes the responsibility to keep things performant. One of the most common culprits of sluggish React apps? Unnecessary re-renders.
This post walks through how React handles re-renders, and more importantly, how you can optimize your components using techniques like memoization, JSX reuse, and context. We’ll build a simple app and use React DevTools to understand what causes a component to re-render—and how to stop it when we don’t need it.
Let’s dive in!
In React, elements are the fundamental building blocks of your UI. JSX gives you a convenient way to define these elements, but under the hood, they’re just objects.
Here’s a quote to keep in mind:
“If you give React the same element you gave it on the last render, it won’t bother re-rendering that element.” — Kent C. Dodds
Let’s see this in action.
function Message({ greeting }) {console.log('rendering greeting', greeting)return <div>{greeting}</div>}function Counter() {const [count, setCount] = useState(0)return (<div><button onClick={() => setCount(c => c + 1)}>The count is {count}</button><Message greeting="Hello!" /></div>)}
With this setup, clicking the button logs every time, even though the <Message />
component receives the same props.
Now look at this slight change:
const message = <Message greeting="Hello!" />function Counter() {const [count, setCount] = useState(0)return (<div><button onClick={() => setCount(c => c + 1)}>The count is {count}</button>{message}</div>)}
Now the Message
component only renders once—even when the count
changes. Why? Because the JSX element is created once outside the render function and reused across renders.
💡 React recognizes it’s the same element and skips the update.
Let’s take this further in a real-world scenario.
Imagine your app has a static <Footer />
that doesn’t depend on state:
function Footer() {console.log('rendering footer')return <footer>© 2025 React Workshop</footer>}function App() {const [count, setCount] = useState(0)return (<div><button onClick={() => setCount(c => c + 1)}>Click</button><Footer /></div>)}
Every button click re-renders <Footer />
, even though it doesn’t need to.
const footer = <Footer />function App() {const [count, setCount] = useState(0)return (<div><button onClick={() => setCount(c => c + 1)}>Click</button>{footer}</div>)}
Boom 💥! The footer is no longer re-rendered needlessly.
Now suppose the Footer
has a dynamic prop like a color
:
function Footer({ color }) {console.log('rendering footer with color', color)return <footer style={{ color }}>Thanks for visiting</footer>}
We can no longer define the JSX statically outside the render function. But we can still optimize.
Let’s use React Context to avoid prop drilling and isolate re-renders.
const ColorContext = React.createContext()
function App() {const [color, setColor] = useState('black')return (<ColorContext.Provider value={color}><Main /></ColorContext.Provider>)}
function Footer() {const color = useContext(ColorContext)console.log('rendering footer with color', color)return <footer style={{ color }}>Thanks!</footer>}
Now, Footer doesn’t receive props, so you can hoist it again and React will only re-render it when context changes.
useMemo
What if you must pass props (like name
) but still want to optimize?
React’s useMemo()
is your friend:
const memoizedFooter = useMemo(() => {return <Footer name={name} />}, [name])
Now <Footer />
is only re-rendered when name
changes—not on every unrelated state update.
💡 Use React DevTools Profiler
to confirm re-render behavior.
React.memo
React even gives you a component-level memoizer: React.memo
.
const Footer = memo(function FooterImpl({ color, name }) {console.log('rendering footer')return <footer style={{ color }}>Hello, {name}!</footer>})
This acts like a built-in useMemo()
for components: React skips re-renders unless props have changed.
This is a cleaner and more robust solution than manually memoizing every element.
⚠️ Be careful:
memo
usesObject.is
under the hood, so if you’re passing complex objects, you may need to memoize them too.
Here’s a quick summary of how to optimize React elements and avoid unnecessary re-renders:
useMemo()
when JSX depends on state.React.memo()
for full component optimization.React is smart—but it’s not omniscient. A few simple tweaks to how and where you define your elements can yield a significant performance boost.
So next time you’re profiling your app and wondering why something is rendering when it shouldn’t—remember this guide 😉
📚 Further reading:
Want a live example or need help debugging your own components? Drop a comment or reach out!
Happy optimizing ⚛️🔥
Quick Links
Legal Stuff
Social Media