For a long time, I assumed React performance issues were usually caused by slow APIs, inefficient algorithms, or expensive database queries. Re-renders never felt like a serious concern. After all, React is designed to update the UI efficiently.
So whenever I noticed a sluggish interface, I instinctively looked everywhere except the component tree.
That assumption turned out to be wrong.
🔍 The Problem Starts Quietly
Initially, everything seemed fine. Components rendered correctly, interactions felt responsive, and development was smooth. There were no obvious signs of performance issues.
However, as the application grew, small delays began to appear. A simple interaction would occasionally feel sluggish. Certain screens became noticeably slower after new features were added. Nothing seemed broken, but the experience no longer felt as responsive as it once had.
The surprising part was that the backend wasn't the problem. Network requests were fast, database queries were efficient, and API responses arrived quickly. Yet users still experienced delays.
Something else was happening beneath the surface.
🧠 The Real Issue Was Never React
My first instinct was to blame React itself. Surely the framework was doing too much work.
However, after profiling the application, it became clear that React wasn't the problem.
The real issue was unnecessary rendering.
A single state update was triggering large portions of the component tree to render again. Components that had no relationship to the changed data were still executing their render logic. What appeared to be one update was actually dozens of updates happening throughout the application.
The problem wasn't that React was slow.
The problem was that I was asking React to do far more work than necessary.
📊 Understanding the Cost of Re-renders
Many developers assume that if the UI doesn't visibly change, a re-render doesn't matter.
In reality, every render still has a cost.
React must execute component functions, evaluate hooks, recalculate JSX, compare virtual DOM trees, and determine whether updates should be committed to the browser.
Individually, these operations are inexpensive. Collectively, they become surprisingly costly when repeated across a large application.
A single unnecessary render may not matter.
Hundreds of unnecessary renders absolutely do.
⚡ Profiling Changed Everything
Rather than immediately reaching for optimisation techniques, I decided to measure the problem first.
Using the React Profiler revealed something I hadn't expected.
The application wasn't suffering from a single expensive component. Instead, it was suffering from a large number of inexpensive components rendering repeatedly.
The issue wasn't concentrated in one place.
It was distributed throughout the system.
That changed how I approached optimisation entirely.
🔬 Pattern #1: Keep State Close to Where It's Used
One of the biggest issues came from state placement.
Over time, state had gradually moved higher and higher in the component hierarchy. It seemed convenient at first, but every update forced large sections of the application to render again.
The solution wasn't adding optimisation hooks.
It was improving architecture.
By keeping state closer to the components that actually consumed it, render boundaries became smaller and updates became more isolated.
The result was fewer unnecessary renders and a much more predictable component tree.
🔗 Pattern #2: Stabilise References Intentionally
The second issue involved objects and functions being recreated on every render.
From React's perspective, a newly created object is different from the previous one, even if it contains the same values. The same applies to functions.
This meant child components were re-rendering simply because their props appeared to change.
Using tools like React.memo, useMemo, and useCallback helped stabilise these references and reduce unnecessary updates.
The important lesson wasn't to memoise everything.
It was to understand why a component was rendering in the first place.
🏗️ Pattern #3: Create Better Rendering Boundaries
Several components had gradually accumulated too many responsibilities.
They handled state, data fetching, business logic, and UI rendering all at once.
As these components grew, even small changes caused large portions of the interface to re-render.
Breaking them into smaller, focused components introduced natural rendering boundaries. Updates became more targeted, and React had significantly less work to perform.
Sometimes performance improvements don't come from optimisation.
They come from better component design.
⚡ What Actually Improved
Once these changes were applied, the difference was immediately noticeable.
Render counts dropped significantly. User interactions became more responsive. Profiling traces became cleaner and easier to reason about. Most importantly, the screen that previously took nearly 800ms to update was now completing the same interaction in roughly 60ms.
The application didn't gain any new features.
It simply stopped performing unnecessary work.
🏛️ Performance Is Often an Architecture Problem
Before this experience, I viewed React performance primarily as an optimisation problem.
I assumed the solution was more memoisation, more hooks, or more advanced techniques.
What I learned instead was that performance is often a consequence of architecture.
Poor state boundaries create unnecessary renders. Large components create unnecessary work. Unstable references create unnecessary updates.
React wasn't slow.
My component structure was.
🔮 Final Thought
Performance issues rarely appear overnight. They accumulate gradually as applications grow and complexity increases.
That is what makes them difficult to spot.
By the time users notice lag, the underlying rendering problem has often existed for months.
The biggest lesson wasn't learning a new optimisation technique.
It was understanding that every state update has a cost, and every re-render should have a reason.
Once you start thinking about React through that lens, performance stops being something you fix at the end and becomes something you design for from the beginning.
Top comments (0)