When you're building the first version of a React app, performance usually isn’t your top concern. Things feel snappy. It’s just React, right?
Then, the app grows. New features get added, state becomes more complex, components nest deeper, and before you know it, something that used to be smooth starts to feel... sluggish.
I’ve been there. Below are some of the most practical performance lessons I’ve learned while scaling React apps—things I wish I’d done earlier, and fixes that made a noticeable difference without overcomplicating the codebase.
1️⃣ Measure Before You Optimize
It sounds obvious, but it’s often skipped. I used to reach for useMemo
or React.memo
way too early without knowing what was actually slowing things down.
What actually helps:
🔍 React DevTools Profiler – Spot unnecessary re-renders.
📦 why-did-you-render – Understand what's rendering and why.
⚙️ Lighthouse – Useful for performance audits and slow paints.
🧪 Real user feedback – Often the best indicator something’s off.
Takeaway: Don’t guess. Measure first, then optimize what’s actually slow.
2️⃣ Too Many Renders? Check Your State
One of the biggest performance drains I’ve seen is over-rendering caused by lifted state. It’s tempting to keep things high up in the tree “just in case” other components need access, but that can end up re-rendering the entire subtree.
What helped:
- Keep state as close to where it’s used as possible.
- Use derived state with memoization.
- Apply
React.memo
only where it helps (after profiling).
Takeaway: Overlifting state leads to over-rendering. Start local, move it up only if needed.
3️⃣ useEffect
Isn't Your Friend (Most of the Time)
In early projects, I used useEffect
for everything: fetching data, syncing state, firing side effects. It worked, but it led to complexity and subtle bugs.
What I changed:
- Stopped using effects for things that could be derived or handled declaratively.
- Moved server state logic to State Manager.
- Created custom hooks to isolate repeated logic.
Takeaway: Reach for useEffect only when there's no better alternative.
4️⃣ Initial Load Times Matter More Than You Think
If your app feels slow to open, users notice. I’ve seen apps load the entire dashboard even when the user just needs the login screen.
What helped:
- ✂️ Code splitting routes using dynamic
import()
andReact.lazy
. - 🧹 Tree shaking unused code and libraries.
- ⚛️ Breaking up giant components into smaller, focused ones.
- 📊 Using tools like
webpack-bundle-analyzer
to see what’s in your bundle.
Takeaway: Don’t serve the whole app upfront. Load what the user needs, when they need it.
5️⃣ Lists Are a Performance Trap
Large lists rendered the wrong way will quietly wreck performance. One project had a simple dropdown with 1000+ items rendered without virtualization—it completely froze the UI.
What helped:
- Used
react-window
to only render visible list items. - Avoided anonymous functions inside
.map()
calls. - Used unique keys (not index) to avoid re-rendering issues.
Takeaway: Lists need extra care. Optimize them early.
6️⃣ Real Debug Example: The Mystery Re-Render
The same seemingly innocent Dropdown component was triggering re-renders across the whole page. It was tied to a parent’s state via props, and every dropdown change re-rendered multiple unrelated charts.
Fix:
- Memoized the dropdown to isolate re-renders.
- Split state into smaller, targeted slices.
- Removed lifted state chain and used shared redux state.
Result: Charts became responsive again, and interaction time dropped by ~200ms.
🧠 Final Thoughts
React performance isn’t about overusing useMemo
or blindly adding caching. It’s about understanding how your app renders, keeping things simple, and avoiding unnecessary work.
Start small. Measure. Fix what matters first.
Often, it’s basic architectural decisions—where state lives, how components are composed, how data flows—that have the biggest impact over time.
Top comments (0)