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
# 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
)
}
```
## 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 (
)
}
```
**Solution:**
```tsx
function Counter() {
const [count, setCount] = useState(0)
return (
)
}
// Memoize static content
const StaticName = React.memo(function StaticName({ name }) {
console.log('StaticName rendered')
return
)
}
function Child({ config }) {
console.log('Child rendered')
return
)
}
const Child = React.memo(function Child({ config }) {
console.log('Child rendered')
return
)
}
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 (
)
}
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
)
}
const Child = React.memo(function Child() {
const { theme } = useContext(AppContext)
console.log('Child rendered')
return {item.name}
}
```
**Solution:**
```tsx
function List({ items }) {
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
)
}
```
### 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 (
)
}
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 }) => (
)
}
```
## Performance Measurement
### Before Optimization
```tsx
function App() {
const [count, setCount] = useState(0)
return (
)
}
function ExpensiveComponent() {
console.log('ExpensiveComponent rendered')
// Simulate expensive operation
const start = performance.now()
while (performance.now() - start < 50) {}
return
)
}
const MemoizedExpensiveComponent = React.memo(function ExpensiveComponent() {
console.log('ExpensiveComponent rendered')
// Simulate expensive operation
const start = performance.now()
while (performance.now() - start < 50) {}
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 (
{count}
{name}
{count}
{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}
{config.theme}
}
```
**Solution:**
```tsx
function Parent() {
const [count, setCount] = useState(0)
// Memoize the object
const config = useMemo(() => ({ theme: 'dark', language: 'en' }), [])
return (
{count}
{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}
{count}
{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 (
{theme}
})
```
### Cause 5: Key Prop Changes
**Problem:**
```tsx
function List({ items }) {
return (
-
{items.map(item => (
// This re-renders the entire item when key changes
-
{items.map(item => (
// Stable key prevents unnecessary re-renders
{/* ... */}
})
```
### 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}
setInputValue(e.target.value)} />
)
}
// Good: State colocated where it's used
function Parent() {
return (
{items[index].name}
)}
{count}
Expensive component
}
```
### After Optimization
```tsx
function App() {
const [count, setCount] = useState(0)
return (
{count}
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...