React Performance Optimization: Beyond the Basics
I opened React DevTools Profiler and watched my app re-render 47 components every time someone typed in a search box.
Forty-seven components. For one keystroke.
Users were complaining about lag. I was embarrassed. Here's how I fixed it.
The Problem: Everything Re-Renders
React re-renders components when state or props change. That's fine. The problem is when components re-render unnecessarily.
I had a search box at the top of my app. Every keystroke updated state. Every state update re-rendered the entire component tree. Even components that had nothing to do with search.
Fix #1: Move State Down
This was the biggest win. I had search state at the top level:
// Bad: State too high up function App() { const [searchTerm, setSearchTerm] = useState('') const [users, setUsers] = useState([]) return ( <div> <Header /> <SearchBox value={searchTerm} onChange={setSearchTerm} /> <UserList users={users} searchTerm={searchTerm} /> <Footer /> </div> ) }
Every keystroke re-rendered Header and Footer even though they don't care about search.
Fixed it by moving state down:
// Good: State where it's needed function App() { return ( <div> <Header /> <UserSearch /> <Footer /> </div> ) } function UserSearch() { const [searchTerm, setSearchTerm] = useState('') const [users, setUsers] = useState([]) return ( <> <SearchBox value={searchTerm} onChange={setSearchTerm} /> <UserList users={users} searchTerm={searchTerm} /> </> ) }
Now only UserSearch and its children re-render. Header and Footer stay untouched.
Re-renders dropped from 47 to 8.
Fix #2: Memoize Expensive Calculations
I had a component that processed data on every render:
// Bad: Recalculates every render function UserList({ users, searchTerm }) { const filteredUsers = users .filter(u => u.name.includes(searchTerm)) .map(u => ({ ...u, displayName: formatName(u) // Expensive operation })) .sort((a, b) => a.displayName.localeCompare(b.displayName)) return <div>{/* render filteredUsers */}</div> }
This ran on every render, even when users and searchTerm hadn't changed.
Fixed with useMemo:
// Good: Only recalculates when inputs change function UserList({ users, searchTerm }) { const filteredUsers = useMemo(() => { return users .filter(u => u.name.includes(searchTerm)) .map(u => ({ ...u, displayName: formatName(u) })) .sort((a, b) => a.displayName.localeCompare(b.displayName)) }, [users, searchTerm]) return <div>{/* render filteredUsers */}</div> }
Now it only recalculates when users or searchTerm actually change.
Fix #3: Code Splitting
My bundle was 380KB. Users on slow connections waited 4-5 seconds for the app to load.
I split out the heavy stuff:
import { lazy, Suspense } from 'react' // Load admin panel only when needed const AdminPanel = lazy(() => import('./AdminPanel')) function App() { return ( <Suspense fallback={<div>Loading...</div>}> {isAdmin && <AdminPanel />} </Suspense> ) }
Main bundle dropped to 180KB. Admin panel loads separately only for admins.
Fix #4: Virtual Lists for Long Lists
I had a list of 500 items. React was rendering all 500, even though only 10 were visible.
Switched to react-window:
import { FixedSizeList } from 'react-window' function UserList({ users }) { return ( <FixedSizeList height={600} itemCount={users.length} itemSize={50} width="100%" > {({ index, style }) => ( <div style={style}> {users[index].name} </div> )} </FixedSizeList> ) }
Now it only renders the visible items plus a few for smooth scrolling. Huge performance improvement.
What I Didn't Need
React.memo everywhere: I tried wrapping every component in React.memo. Made things worse. Only use it for components that re-render often with the same props.
useCallback for everything: Same deal. Only use it when you're passing callbacks to memoized children.
Rewriting in a different framework: Considered switching to Svelte or Solid. Didn't need to. React is fine when you use it right.
Measuring Performance
Don't guess. Measure.
I use React DevTools Profiler to see what's actually slow:
import { Profiler } from 'react' function onRender(id, phase, actualDuration) { if (actualDuration > 16) { // Slower than 60fps console.log(`${id} took ${actualDuration}ms`) } } <Profiler id="UserList" onRender={onRender}> <UserList /> </Profiler>
This showed me that UserList was the problem, not Header or Footer.
The Results
Before:
- 47 components re-rendering per keystroke
- 380KB bundle size
- 4-5 second load time on slow connections
- Visible lag when typing
After:
- 8 components re-rendering per keystroke
- 180KB main bundle
- 1.5 second load time
- No lag
Cost: $0. Just better code.
Should You Optimize?
Not yet.
Build your app first. Make it work. Then, if it's slow, measure and optimize.
Don't prematurely optimize. Don't wrap everything in useMemo and React.memo "just in case." You'll make your code harder to read for no benefit.
But when you do have performance issues, these techniques actually work.
Fighting React performance issues? I'd love to hear what worked for you. Hit me up on LinkedIn.