Back to Blog
August 19, 20268 min read1 views

Why Your React App Re-renders 20 Times Per Second (And How to Fix It)

Next.jsTypeScriptWeb Development
JL
Written by Julius Legaspi
LinkedInGitHub
Share
# Why Your React App Re-renders 20 Times Per Second (And How to Fix It) ## Introduction You've built your React application, and it's feeling sluggish. Pages take forever to load, interactions feel delayed, and your users are complaining. The culprit? Unnecessary re-renders. Re-renders are when React re-executes a component's render function, even though the output would be the same. While some re-renders are necessary, excessive re-renders can cripple performance. After optimizing performance in several production React applications, I've developed a systematic approach to identifying and fixing re-render issues. ## Understanding Re-renders ### What Causes Re-renders? React re-renders a component when: 1. **State changes** — `useState` or `useReducer` updates 2. **Props change** — Parent component re-renders and passes new props 3. **Context changes** — Context value updates 4. **Parent re-renders** — Even if props don't change! ### The Re-render Cascade When a parent component re-renders, all its children re-render too, unless they're memoized: ``` Parent (re-renders) ├── Child1 (re-renders) ├── Child2 (re-renders) │ ├── Grandchild1 (re-renders) │ └── Grandchild2 (re-renders) └── Child3 (re-renders) ``` This cascade can quickly spiral out of control. ## Identifying Re-render Issues ### 1. Use React DevTools Profiler The React DevTools Profiler is your best friend for identifying re-render issues: 1. Install React DevTools browser extension 2. Open Developer Tools → Profiler 3. Click "Record" and interact with your app 4. Stop recording and analyze the results Look for: - Components that re-render frequently - Components that re-render with the same props - Components that take a long time to render ### 2. Add Performance Logging Add logging to identify re-renders: ```tsx function ExpensiveComponent({ data }) { console.log('ExpensiveComponent rendered') // Expensive computation const processedData = data.map(item => ({ ...item, computed: expensiveCalculation(item) })) return
{/* ... */}
} ``` ### 3. Use React.Profiler Wrap components with `React.Profiler` to measure render times: ```tsx import { Profiler } from 'react' function onRenderCallback( id, phase, actualDuration, baseDuration, startTime, commitTime ) { console.log(`${id} took ${actualDuration}ms to render`) } function App() { return ( ) } ``` ## Common Re-render Causes and Solutions ### Cause 1: Unnecessary State Updates **Problem:** ```tsx function Counter() { const [count, setCount] = useState(0) const [name, setName] = useState('John') return (

{count}

{name}

) } ``` **Solution:** ```tsx function Counter() { const [count, setCount] = useState(0) return (

{count}

) } // Memoize static content const StaticName = React.memo(function StaticName({ name }) { console.log('StaticName rendered') return

{name}

}) ``` ### Cause 2: Object/Array Props **Problem:** ```tsx function Parent() { const [count, setCount] = useState(0) // This object is recreated on every render const config = { theme: 'dark', language: 'en' } return (

{count}

) } function Child({ config }) { console.log('Child rendered') return
{config.theme}
} ``` **Solution:** ```tsx function Parent() { const [count, setCount] = useState(0) // Memoize the object const config = useMemo(() => ({ theme: 'dark', language: 'en' }), []) return (

{count}

) } const Child = React.memo(function Child({ config }) { console.log('Child rendered') return
{config.theme}
}) ``` ### Cause 3: Inline Functions **Problem:** ```tsx function Parent() { const [count, setCount] = useState(0) // This function is recreated on every render const handleClick = () => { console.log('Button clicked') } return (

{count}

) } function Child({ onClick }) { console.log('Child rendered') return } ``` **Solution:** ```tsx function Parent() { const [count, setCount] = useState(0) // Memoize the function const handleClick = useCallback(() => { console.log('Button clicked') }, []) return (

{count}

) } const Child = React.memo(function Child({ onClick }) { console.log('Child rendered') return }) ``` ### Cause 4: Context Changes **Problem:** ```tsx const AppContext = createContext() function App() { const [theme, setTheme] = useState('dark') const [user, setUser] = useState(null) // This object is recreated on every render const contextValue = { theme, setTheme, user, setUser } return ( ) } function Child() { const { theme } = useContext(AppContext) console.log('Child rendered') return
{theme}
} ``` **Solution:** ```tsx const AppContext = createContext() function App() { const [theme, setTheme] = useState('dark') const [user, setUser] = useState(null) // Memoize the context value const contextValue = useMemo( () => ({ theme, setTheme, user, setUser }), [theme, user] ) return ( ) } const Child = React.memo(function Child() { const { theme } = useContext(AppContext) console.log('Child rendered') return
{theme}
}) ``` ### Cause 5: Key Prop Changes **Problem:** ```tsx function List({ items }) { return (
    {items.map(item => ( // This re-renders the entire item when key changes ))}
) } function ListItem({ item }) { console.log('ListItem rendered') return
  • {item.name}
  • } ``` **Solution:** ```tsx function List({ items }) { return (
      {items.map(item => ( // Stable key prevents unnecessary re-renders ))}
    ) } const ListItem = React.memo(function ListItem({ item }) { console.log('ListItem rendered') return
  • {item.name}
  • }) ``` ## Advanced Optimization Techniques ### 1. React.memo Wrap components with `React.memo` to prevent re-renders when props don't change: ```tsx const ExpensiveComponent = React.memo(function ExpensiveComponent({ data }) { console.log('ExpensiveComponent rendered') const processedData = data.map(item => ({ ...item, computed: expensiveCalculation(item) })) return
    {/* ... */}
    }) ``` ### 2. useMemo Memoize expensive computations: ```tsx function DataProcessor({ data }) { // Only recompute when data changes const processedData = useMemo(() => { return data.map(item => ({ ...item, computed: expensiveCalculation(item) })) }, [data]) return
    {/* ... */}
    } ``` ### 3. useCallback Memoize functions passed as props: ```tsx function Parent() { const [count, setCount] = useState(0) const handleClick = useCallback(() => { console.log('Button clicked') }, []) return (

    {count}

    ) } ``` ### 4. State Colocation Move state as close to where it's used as possible: ```tsx // Bad: State in parent causes unnecessary re-renders function Parent() { const [inputValue, setInputValue] = useState('') return (
    setInputValue(e.target.value)} />
    ) } // Good: State colocated where it's used function Parent() { return (
    ) } function Input() { const [inputValue, setInputValue] = useState('') return setInputValue(e.target.value)} /> } ``` ### 5. Virtualization For long lists, use virtualization to render only visible items: ```tsx import { FixedSizeList } from 'react-window' function VirtualizedList({ items }) { return ( {({ index, style }) => (
    {items[index].name}
    )}
    ) } ``` ## Performance Measurement ### Before Optimization ```tsx function App() { const [count, setCount] = useState(0) return (

    {count}

    ) } function ExpensiveComponent() { console.log('ExpensiveComponent rendered') // Simulate expensive operation const start = performance.now() while (performance.now() - start < 50) {} return
    Expensive component
    } ``` ### After Optimization ```tsx function App() { const [count, setCount] = useState(0) return (

    {count}

    ) } const MemoizedExpensiveComponent = React.memo(function ExpensiveComponent() { console.log('ExpensiveComponent rendered') // Simulate expensive operation const start = performance.now() while (performance.now() - start < 50) {} return
    Expensive component
    }) ``` ## Common Mistakes ### 1. Over-optimizing Don't optimize everything upfront. Profile first, then optimize the bottlenecks. ### 2. Using useMemo for Everything Not every computation needs memoization. Only memoize expensive operations. ### 3. Forgetting Dependencies Always include all dependencies in `useMemo` and `useCallback`: ```tsx // Bad: Missing dependency const result = useMemo(() => { return data.filter(item => item.category === category) }, [data]) // Missing category! // Good: All dependencies included const result = useMemo(() => { return data.filter(item => item.category === category) }, [data, category]) ``` ### 4. Not Using React.memo Correctly `React.memo` only prevents re-renders when props are shallowly equal. For complex props, you may need a custom comparison function: ```tsx const ExpensiveComponent = React.memo( function ExpensiveComponent({ data }) { return
    {/* ... */}
    }, (prevProps, nextProps) => { // Custom comparison logic return prevProps.data.id === nextProps.data.id } ) ``` ## Conclusion Re-renders are a natural part of React, but unnecessary re-renders can cripple performance. By understanding what causes re-renders and using the right optimization techniques, you can build performant React applications. **Key takeaways:** 1. **Profile first** — Use React DevTools to identify bottlenecks 2. **Memoize strategically** — Don't optimize everything, focus on expensive operations 3. **Stabilize props** — Use `useMemo` and `useCallback` for objects and functions 4. **Colocate state** — Move state close to where it's used 5. **Virtualize lists** — For long lists, render only visible items The key insight is that re-renders are not inherently bad — they're how React keeps your UI in sync with state. The goal is to eliminate unnecessary re-renders while keeping the ones that matter. --- **Need help optimizing your React application's performance?** [Contact me](/contact) to discuss your performance issues.

    Comments (0)

    Loading comments...