React.memo Is Not Enough — 4 Performance Fixes Senior Devs Actually Use

Last month, I opened our React dashboard and watched it take 6 full seconds to load.
Six seconds. In 2026. Unacceptable.
Users were complaining. Bounce rate was climbing. And every time I clicked a button, the UI would freeze for half a second before responding.
I spent one day diagnosing and fixing it. The app now loads in under 1.5 seconds.
Here's exactly what I found — and how I fixed it. 🚀
Step 0: Never Optimize Without Measuring First
This is the mistake most developers make. They guess what's slow and start randomly adding useMemo and React.memo everywhere.
Don't do that.
Before touching a single line of code, open React DevTools Profiler:
- Install React DevTools in Chrome
- Open DevTools → Profiler tab
- Click Record
- Interact with your slow component
- Stop recording → Look at the Flame Chart
💡 What to look for: Any component taking more than 50ms is a red flag. In 2026, Google measures Interaction to Next Paint (INP) — anything blocking the main thread hurts your SEO and user experience.
In my case, the profiler revealed 3 major problems. Here's each one and how I fixed it.
Problem 1: Unnecessary Re-Renders Everywhere
This was the biggest culprit. Our parent component was re-rendering on every state change — and dragging every single child component with it.
The broken code:
// ❌ Every time `count` changes, ALL children re-render
function Dashboard() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>Update</button>
<HeavyChart /> {/* Re-renders even though it doesn't use count */}
<UserTable /> {/* Same problem */}
<ActivityFeed /> {/* Same problem */}
</>
);
}
Every click on that button was re-rendering HeavyChart, UserTable, and ActivityFeed — even though none of them used count.
The fix — React.memo:
// ✅ Now these components only re-render when their own props change
const HeavyChart = React.memo(function HeavyChart() {
return <div>...chart...</div>;
});
const UserTable = React.memo(function UserTable({ users }) {
return <table>...table...</table>;
});
Result: Re-renders dropped by 70% for these components.
💡 2026 Update: If you're using the React Compiler (formerly React Forget), it handles memoization automatically. But if you haven't set it up yet,
React.memois still your best friend.
Problem 2: A 95KB JavaScript Bundle Loaded Upfront
Our app was shipping one massive JavaScript file containing every page, every component, and every library — even for pages the user might never visit.
I ran Webpack Bundle Analyzer to see what was inside:
npm install --save-dev webpack-bundle-analyzer
npx webpack-bundle-analyzer build/static/js/*.js
What I found was shocking. A charting library we used on ONE page was taking up 38KB of our bundle — loading for every single user on every single page.
The fix — Lazy Loading with React.lazy:
// ❌ Before — everything loaded upfront
import HeavyChartPage from './pages/HeavyChartPage';
import AdminPanel from './pages/AdminPanel';
import ReportsPage from './pages/ReportsPage';
// ✅ After — only loaded when user visits that page
import { lazy, Suspense } from 'react';
const HeavyChartPage = lazy(() => import('./pages/HeavyChartPage'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));
const ReportsPage = lazy(() => import('./pages/ReportsPage'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/charts" element={<HeavyChartPage />} />
<Route path="/admin" element={<AdminPanel />} />
<Route path="/reports" element={<ReportsPage />} />
</Routes>
</Suspense>
);
}
Result: Initial bundle size dropped from 95KB to 31KB. Load time went from 6 seconds to 2.8 seconds — just from this one change.
Problem 3: Expensive Calculations Running on Every Render
Our dashboard had a filtering + sorting feature for a table with 2,000+ rows. The filter function was running on every single render — even when the data hadn't changed.
// ❌ This runs on every render — even unrelated ones
function UserTable({ users, searchQuery }) {
// This heavy calculation runs EVERY time
const filteredUsers = users
.filter(user => user.name.includes(searchQuery))
.sort((a, b) => a.name.localeCompare(b.name));
return <table>...</table>;
}
The fix — useMemo:
// ✅ Only recalculates when users or searchQuery actually changes
function UserTable({ users, searchQuery }) {
const filteredUsers = useMemo(() => {
return users
.filter(user => user.name.includes(searchQuery))
.sort((a, b) => a.name.localeCompare(b.name));
}, [users, searchQuery]);
return <table>...</table>;
}
Result: The table interaction went from 340ms to 12ms. Instantly noticeable.
Bonus Fix: List Virtualization for Long Lists
Even after memoizing, rendering 2,000+ table rows in the DOM was still slow. The browser was creating 2,000 DOM nodes even though the user could only see 20 at a time.
The fix — react-window:
npm install react-window
import { FixedSizeList } from 'react-window';
// ❌ Before — renders ALL 2000 rows in the DOM
function UserList({ users }) {
return (
<div>
{users.map(user => <UserRow key={user.id} user={user} />)}
</div>
);
}
// ✅ After — only renders ~20 visible rows at any time
function UserList({ users }) {
const Row = ({ index, style }) => (
<div style={style}>
<UserRow user={users[index]} />
</div>
);
return (
<FixedSizeList
height={600}
itemCount={users.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
Result: Scrolling through 2,000 rows became buttery smooth.
The Final Results
Here's what one day of focused performance work achieved:
| Metric | Before | After |
|---|---|---|
| Initial Load Time | 6.0s | 1.4s |
| Bundle Size | 95KB | 31KB |
| Table Interaction | 340ms | 12ms |
| Re-renders per click | ~47 | ~8 |
Your React Performance Checklist
Before optimizing, always profile first. Then work through this list:
1. ✅ Run React DevTools Profiler — find what's actually slow
2. ✅ Add React.memo to components that re-render unnecessarily
3. ✅ Run Webpack Bundle Analyzer — find what's bloating your bundle
4. ✅ Lazy load routes and heavy components with React.lazy
5. ✅ Wrap expensive calculations in useMemo
6. ✅ Virtualize long lists with react-window
7. ✅ Enable React Compiler if on React 19+ — auto memoization!
The Real Lesson
Performance issues don't announce themselves. They sneak in slowly — one re-render here, one large import there — until suddenly your app feels like it's running through mud.
The fix isn't always complicated. In my case, four targeted changes — memoization, code splitting, useMemo, and list virtualization — cut load time by 77%.
The key is to measure first, fix second. Never optimize blindly.
Have you had a React performance nightmare in your own project? What was the culprit? Drop it in the comments — I'd love to compare war stories! 👇
Heads up: AI helped me write this.But the ideas, code review, and learning are all mine — AI just helped me communicate them better. I believe in being transparent about my process! 😊