React Performance Optimization: A Practical Guide
React apps can become slow as they grow. This guide covers practical techniques to identify bottlenecks and optimize performance, backed by real-world examples.
Common Performance Issues
Before optimizing, measure. Use React DevTools Profiler to identify:
- Unnecessary re-renders
- Heavy computations
- Large component trees
- Inefficient list rendering
1. Prevent Unnecessary Re-renders
Problem: Parent Re-renders Cascade
// ❌ Bad: Child re-renders on every parent update
function Parent() {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ExpensiveChild /> {/* Re-renders even though it doesn't use count! */}
</div>
)
}
Solution: React.memo
// ✅ Good: Child only re-renders when props change
const ExpensiveChild = React.memo(() => {
console.log('ExpensiveChild rendered')
return <div>{/* expensive rendering */}</div>
})
When to use: Components that render often but rarely change props.
2. Optimize Expensive Calculations
Problem: Recalculating on Every Render
// ❌ Bad: filterItems runs on every render
function ProductList({ products, searchTerm }) {
const filtered = products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
) // Runs even when products/searchTerm haven't changed!
return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}
Solution: useMemo
// ✅ Good: Only recalculates when dependencies change
function ProductList({ products, searchTerm }) {
const filtered = useMemo(() =>
products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
),
[products, searchTerm]
)
return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}
When to use: Expensive calculations that don’t need to run on every render.
3. Stabilize Function References
Problem: New Function on Every Render
// ❌ Bad: handleClick creates new function every render
function Parent() {
const [count, setCount] = useState(0)
const handleClick = () => setCount(count + 1)
return <Child onClick={handleClick} /> // Child re-renders!
}
const Child = React.memo(({ onClick }) => {
return <button onClick={onClick}>Click</button>
})
Solution: useCallback
// ✅ Good: handleClick reference stays stable
function Parent() {
const [count, setCount] = useState(0)
const handleClick = useCallback(() => {
setCount(c => c + 1) // Use functional update!
}, []) // Empty deps because we use functional update
return <Child onClick={handleClick} />
}
When to use: Passing callbacks to memoized child components.
4. Virtual Lists for Large Datasets
Problem: Rendering 10,000 Items
// ❌ Bad: Renders all 10,000 items (slow!)
function LargeList({ items }) {
return (
<div>
{items.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
)
}
Solution: react-window
// ✅ Good: Only renders visible items
import { FixedSizeList } from 'react-window'
function LargeList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>{items[index].name}</div>
)
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
)
}
Result: 10,000 items render in <50ms instead of 2000ms!
5. Code Splitting & Lazy Loading
Problem: Large Bundle Size
// ❌ Bad: Everything loads upfront
import AdminPanel from './AdminPanel' // 500KB!
import UserDashboard from './UserDashboard'
function App() {
return isAdmin ? <AdminPanel /> : <UserDashboard />
}
Solution: React.lazy
// ✅ Good: Load components on demand
const AdminPanel = React.lazy(() => import('./AdminPanel'))
const UserDashboard = React.lazy(() => import('./UserDashboard'))
function App() {
return (
<Suspense fallback={<Loading />}>
{isAdmin ? <AdminPanel /> : <UserDashboard />}
</Suspense>
)
}
Result: Initial bundle: 150KB instead of 650KB!
6. Optimize Context Usage
Problem: Context Causes Mass Re-renders
// ❌ Bad: Every consumer re-renders on any state change
const AppContext = createContext()
function AppProvider({ children }) {
const [user, setUser] = useState(null)
const [theme, setTheme] = useState('light')
const [notifications, setNotifications] = useState([])
return (
<AppContext.Provider value={{ user, theme, notifications, setUser, setTheme }}>
{children}
</AppContext.Provider>
)
}
Solution: Split Contexts
// ✅ Good: Separate contexts for different concerns
const UserContext = createContext()
const ThemeContext = createContext()
const NotificationContext = createContext()
function AppProvider({ children }) {
const [user, setUser] = useState(null)
const [theme, setTheme] = useState('light')
const [notifications, setNotifications] = useState([])
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<NotificationContext.Provider value={{ notifications }}>
{children}
</NotificationContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
)
}
Result: Components only re-render when their specific context changes!
7. Debounce Expensive Operations
Problem: Search Triggers on Every Keystroke
// ❌ Bad: API call on every keystroke
function SearchBox() {
const [query, setQuery] = useState('')
useEffect(() => {
searchAPI(query) // Called 10+ times as user types "javascript"!
}, [query])
return <input value={query} onChange={e => setQuery(e.target.value)} />
}
Solution: Custom Debounce Hook
// ✅ Good: API call only after user stops typing
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const handler = setTimeout(() => setDebouncedValue(value), delay)
return () => clearTimeout(handler)
}, [value, delay])
return debouncedValue
}
function SearchBox() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
useEffect(() => {
if (debouncedQuery) {
searchAPI(debouncedQuery) // Called once after 300ms pause!
}
}, [debouncedQuery])
return <input value={query} onChange={e => setQuery(e.target.value)} />
}
8. Key Props Matter
Problem: Poor Key Choices
// ❌ Bad: Using index as key causes issues
{items.map((item, index) => (
<div key={index}>{item.name}</div>
))}
// When items reorder, React thinks they're different components!
Solution: Stable, Unique Keys
// ✅ Good: Use stable IDs
{items.map(item => (
<div key={item.id}>{item.name}</div>
))}
Real-World Example: Optimizing a Dashboard
Before: Dashboard with 20 widgets, each fetching data, all re-rendering on any state change.
After optimization:
- Split into separate contexts for user, theme, data
- Memoized individual widgets with React.memo
- Used useMemo for data transformations
- Lazy loaded heavy charts
- Debounced filter inputs
Result:
- Initial render: 3000ms → 800ms
- Interactions: 200ms → 50ms
- Bundle size: 800KB → 250KB (initial) + 550KB (lazy)
Profiling Workflow
React DevTools Profiler
- Record interaction
- Identify components taking longest
- Check why they rendered (props change? parent render?)
Chrome DevTools Performance
- Record performance
- Look for long tasks (>50ms)
- Check FPS during animations
Lighthouse
- Run audit
- Focus on “Time to Interactive”
- Check bundle sizes
Common Pitfalls
❌ Don’t optimize prematurely
✅ Do measure first, optimize bottlenecks
❌ Don’t memo everything
✅ Do memo expensive components
❌ Don’t use useMemo for simple calculations
✅ Do use for actual performance issues
❌ Don’t forget dependency arrays
✅ Do include all dependencies (or use ESLint plugin)
Performance Checklist
- Use React DevTools Profiler to identify issues
- Memoize expensive child components
- Use useMemo for heavy calculations
- Use useCallback for callbacks to memoized children
- Implement code splitting for large components
- Use virtual lists for large datasets
- Debounce expensive operations (search, API calls)
- Use proper key props for lists
- Split contexts to minimize re-renders
- Optimize bundle size (tree shaking, minification)
Tools & Resources
Conclusion
Performance optimization is about:
- Measuring before optimizing
- Identifying actual bottlenecks
- Applying targeted fixes
- Verifying improvements
Don’t optimize blindly—profile, fix, measure, repeat!
Want to dive deeper? Check out my React Performance Workshop.
Questions? Reach out on Twitter!

Mobil tarafta Buyingo ve Remingo uygulamalarını geliştirdim. Ayrıca hisse senetleri için Python tabanlı, web arayüzlü bir yatırım karar destek sistemi üzerinde çalışıyorum.
Proje Linkleri: Buyingo · Remingo · Bist Karar Destek Web Arayüzü
