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

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

Source: Dev.to

Step 0: Never Optimize Without Measuring First ## Problem 1: Unnecessary Re-Renders Everywhere ## Problem 2: A 95KB JavaScript Bundle Loaded Upfront ## Problem 3: Expensive Calculations Running on Every Render ## Bonus Fix: List Virtualization for Long Lists ## The Final Results ## Your React Performance Checklist ## The Real Lesson 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. 🚀 This is the mistake most developers make. They guess what's slow and start randomly adding useMemo and React.memo everywhere. Before touching a single line of code, open React DevTools Profiler: 💡 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. This was the biggest culprit. Our parent component was re-rendering on every state change — and dragging every single child component with it. Every click on that button was re-rendering HeavyChart, UserTable, and ActivityFeed — even though none of them used count. The fix — React.memo: 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.memo is still your best friend. 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: 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: Result: Initial bundle size dropped from 95KB to 31KB. Load time went from 6 seconds to 2.8 seconds — just from this one change. 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. Result: The table interaction went from 340ms to 12ms. Instantly noticeable. 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: Result: Scrolling through 2,000 rows became buttery smooth. Here's what one day of focused performance work achieved: Before optimizing, always profile first. Then work through this list: 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! 😊 Templates let you quickly answer FAQs or store snippets for re-use. hey! i'm a beginner web developer ofcourse still learning and exploring things, the unnecessary re-render issue it happens with me everytime i tried to built something the problem is i can't understand that i don't need to re-render these things always but the states trigger them , i used to create separate props and states to solve this but now i know that there's other simpler way thanks this post really gave me something valuable. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: // ❌ 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 */} </> ); } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // ❌ 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 */} </> ); } COMMAND_BLOCK: // ❌ 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 */} </> ); } CODE_BLOCK: // ✅ 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>; }); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // ✅ 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>; }); CODE_BLOCK: // ✅ 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>; }); COMMAND_BLOCK: npm install --save-dev webpack-bundle-analyzer npx webpack-bundle-analyzer build/static/js/*.js Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: npm install --save-dev webpack-bundle-analyzer npx webpack-bundle-analyzer build/static/js/*.js COMMAND_BLOCK: npm install --save-dev webpack-bundle-analyzer npx webpack-bundle-analyzer build/static/js/*.js COMMAND_BLOCK: // ❌ 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> ); } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // ❌ 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> ); } COMMAND_BLOCK: // ❌ 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> ); } COMMAND_BLOCK: // ❌ 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>; } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // ❌ 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>; } COMMAND_BLOCK: // ❌ 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>; } COMMAND_BLOCK: // ✅ 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>; } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // ✅ 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>; } COMMAND_BLOCK: // ✅ 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>; } COMMAND_BLOCK: npm install react-window Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: npm install react-window COMMAND_BLOCK: npm install react-window COMMAND_BLOCK: 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> ); } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: 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> ); } COMMAND_BLOCK: 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> ); } CODE_BLOCK: 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! Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: 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! CODE_BLOCK: 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! - Install React DevTools in Chrome - Open DevTools → Profiler tab - Click Record - Interact with your slow component - Stop recording → Look at the Flame Chart - Joined Mar 1, 2026