# React Performance Optimization Guide
## Overview
This guide covers performance optimization strategies for React 19 applications.
## Measurement & Profiling
### React DevTools Profiler
Record performance data:
1. Open React DevTools
2. Go to Profiler tab
3. Click record button
4. Interact with app
5. Stop recording
6. Analyze flame graph and ranked chart
### Profiler Component
```typescript
import { Profiler } from 'react'
const App = () => {
const onRender = (
id: string,
phase: 'mount' | 'update',
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number
) => {
console.log({
component: id,
phase,
actualDuration, // Time spent rendering this update
baseDuration // Estimated time without memoization
})
}
return (
)
}
```
### Performance Metrics
```typescript
// Custom performance tracking
const startTime = performance.now()
// ... do work
const endTime = performance.now()
console.log(`Operation took ${endTime - startTime}ms`)
// React rendering metrics
import { unstable_trace as trace } from 'react'
trace('expensive-operation', async () => {
await performExpensiveOperation()
})
```
## Memoization Strategies
### React.memo
Prevent unnecessary re-renders:
```typescript
// Basic memoization
const ExpensiveComponent = memo(({ data }: Props) => {
return
{processData(data)}
})
// Custom comparison
const MemoizedComponent = memo(
({ user }: Props) => ,
(prevProps, nextProps) => {
// Return true if props are equal (skip render)
return prevProps.user.id === nextProps.user.id
}
)
```
**When to use:**
- Component renders often with same props
- Rendering is expensive
- Component receives complex prop objects
**When NOT to use:**
- Props change frequently
- Component is already fast
- Premature optimization
### useMemo
Memoize computed values:
```typescript
const SortedList = ({ items, filter }: Props) => {
// Without memoization - runs every render
const filteredItems = items.filter(item => item.type === filter)
const sortedItems = filteredItems.sort((a, b) => a.name.localeCompare(b.name))
// With memoization - only runs when dependencies change
const sortedFilteredItems = useMemo(() => {
const filtered = items.filter(item => item.type === filter)
return filtered.sort((a, b) => a.name.localeCompare(b.name))
}, [items, filter])
return (
{sortedFilteredItems.map(item => (
- {item.name}
))}
)
}
```
**When to use:**
- Expensive calculations (sorting, filtering large arrays)
- Creating stable object references
- Computed values used as dependencies
### useCallback
Memoize callback functions:
```typescript
const Parent = () => {
const [count, setCount] = useState(0)
// Without useCallback - new function every render
const handleClick = () => {
setCount(c => c + 1)
}
// With useCallback - stable function reference
const handleClickMemo = useCallback(() => {
setCount(c => c + 1)
}, [])
return
}
const MemoizedChild = memo(({ onClick }: Props) => {
return
})
```
**When to use:**
- Passing callbacks to memoized components
- Callback is used in dependency array
- Callback is expensive to create
## React Compiler (Automatic Optimization)
### Enable React Compiler
React 19 can automatically optimize without manual memoization:
```javascript
// babel.config.js
module.exports = {
plugins: [
['react-compiler', {
compilationMode: 'all', // Optimize all components
}]
]
}
```
### Compilation Modes
```javascript
{
compilationMode: 'annotation', // Only components with "use memo"
compilationMode: 'all', // All components (recommended)
compilationMode: 'infer' // Based on component complexity
}
```
### Directives
```typescript
// Force memoization
'use memo'
const Component = ({ data }: Props) => {
return {data}
}
// Prevent memoization
'use no memo'
const SimpleComponent = ({ text }: Props) => {
return {text}
}
```
## State Management Optimization
### State Colocation
Keep state as close as possible to where it's used:
```typescript
// Bad - state too high
const App = () => {
const [showModal, setShowModal] = useState(false)
return (
<>
setShowModal(false)} />
>
)
}
// Good - state colocated
const App = () => {
return (
<>
>
)
}
const ModalContainer = () => {
const [showModal, setShowModal] = useState(false)
return setShowModal(false)} />
}
```
### Split Context
Avoid unnecessary re-renders by splitting context:
```typescript
// Bad - single context causes all consumers to re-render
const AppContext = createContext({ user, theme, settings })
// Good - split into separate contexts
const UserContext = createContext(user)
const ThemeContext = createContext(theme)
const SettingsContext = createContext(settings)
```
### Context with useMemo
```typescript
const ThemeProvider = ({ children }: Props) => {
const [theme, setTheme] = useState('light')
// Memoize context value to prevent unnecessary re-renders
const value = useMemo(() => ({
theme,
setTheme
}), [theme])
return (
{children}
)
}
```
## Code Splitting & Lazy Loading
### React.lazy
Split components into separate bundles:
```typescript
import { lazy, Suspense } from 'react'
// Lazy load components
const Dashboard = lazy(() => import('./Dashboard'))
const Settings = lazy(() => import('./Settings'))
const Profile = lazy(() => import('./Profile'))
const App = () => {
return (
}>
} />
} />
} />
)
}
```
### Route-based Splitting
```typescript
// App.tsx
const routes = [
{ path: '/', component: lazy(() => import('./pages/Home')) },
{ path: '/about', component: lazy(() => import('./pages/About')) },
{ path: '/products', component: lazy(() => import('./pages/Products')) },
]
const App = () => (
}>
{routes.map(({ path, component: Component }) => (
} />
))}
)
```
### Component-based Splitting
```typescript
// Split expensive components
const HeavyChart = lazy(() => import('./HeavyChart'))
const Dashboard = () => {
const [showChart, setShowChart] = useState(false)
return (
<>
{showChart && (
}>
)}
>
)
}
```
## List Rendering Optimization
### Keys
Always use stable, unique keys:
```typescript
// Bad - index as key (causes issues on reorder/insert)
{items.map((item, index) => (
))}
// Good - unique ID as key
{items.map(item => (
))}
// For static lists without IDs
{items.map(item => (
))}
```
### Virtualization
For long lists, render only visible items:
```typescript
import { useVirtualizer } from '@tanstack/react-virtual'
const VirtualList = ({ items }: { items: Item[] }) => {
const parentRef = useRef(null)
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50, // Estimated item height
overscan: 5 // Render 5 extra items above/below viewport
})
return (
{virtualizer.getVirtualItems().map(virtualItem => (
))}
)
}
```
### Pagination
```typescript
const PaginatedList = ({ items }: Props) => {
const [page, setPage] = useState(1)
const itemsPerPage = 20
const paginatedItems = useMemo(() => {
const start = (page - 1) * itemsPerPage
const end = start + itemsPerPage
return items.slice(start, end)
}, [items, page, itemsPerPage])
return (
<>
{paginatedItems.map(item => (
))}
>
)
}
```
## Transitions & Concurrent Features
### useTransition
Keep UI responsive during expensive updates:
```typescript
const SearchPage = () => {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
const handleSearch = (value: string) => {
setQuery(value) // Urgent - update input immediately
// Non-urgent - can be interrupted
startTransition(() => {
const filtered = expensiveFilter(items, value)
setResults(filtered)
})
}
return (
<>
handleSearch(e.target.value)} />
{isPending && }
>
)
}
```
### useDeferredValue
Defer non-urgent renders:
```typescript
const SearchPage = () => {
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
// Input updates immediately
// Results update with deferred value (can be interrupted)
const results = useMemo(() => {
return expensiveFilter(items, deferredQuery)
}, [deferredQuery])
return (
<>
setQuery(e.target.value)} />
>
)
}
```
## Image & Asset Optimization
### Lazy Load Images
```typescript
const LazyImage = ({ src, alt }: Props) => {
const [isLoaded, setIsLoaded] = useState(false)
return (
{!isLoaded &&
}

setIsLoaded(true)}
className={isLoaded ? 'opacity-100' : 'opacity-0'}
/>
)
}
```
### Next.js Image Component
```typescript
import Image from 'next/image'
const OptimizedImage = () => (
)
```
## Bundle Size Optimization
### Tree Shaking
Import only what you need:
```typescript
// Bad - imports entire library
import _ from 'lodash'
// Good - import only needed functions
import debounce from 'lodash/debounce'
import throttle from 'lodash/throttle'
// Even better - use native methods when possible
const debounce = (fn, delay) => {
let timeoutId
return (...args) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => fn(...args), delay)
}
}
```
### Analyze Bundle
```bash
# Next.js
ANALYZE=true npm run build
# Create React App
npm install --save-dev webpack-bundle-analyzer
```
### Dynamic Imports
```typescript
// Load library only when needed
const handleExport = async () => {
const { jsPDF } = await import('jspdf')
const doc = new jsPDF()
doc.save('report.pdf')
}
```
## Common Performance Pitfalls
### 1. Inline Object Creation
```typescript
// Bad - new object every render
// Good - stable reference
const style = { margin: 10 }
// Or use useMemo
const style = useMemo(() => ({ margin: 10 }), [])
```
### 2. Inline Functions
```typescript
// Bad - new function every render (if child is memoized)
handleClick(id)} />
// Good
const handleClickMemo = useCallback(() => handleClick(id), [id])
```
### 3. Spreading Props
```typescript
// Bad - causes re-renders even when props unchanged
// Good - pass only needed props
```
### 4. Large Context
```typescript
// Bad - everything re-renders on any state change
const AppContext = createContext({ user, theme, cart, settings, ... })
// Good - split into focused contexts
const UserContext = createContext(user)
const ThemeContext = createContext(theme)
const CartContext = createContext(cart)
```
## Performance Checklist
- [ ] Measure before optimizing (use Profiler)
- [ ] Use React DevTools to identify slow components
- [ ] Implement code splitting for large routes
- [ ] Lazy load below-the-fold content
- [ ] Virtualize long lists
- [ ] Memoize expensive calculations
- [ ] Split large contexts
- [ ] Colocate state close to usage
- [ ] Use transitions for non-urgent updates
- [ ] Optimize images and assets
- [ ] Analyze and minimize bundle size
- [ ] Remove console.logs in production
- [ ] Use production build for testing
- [ ] Monitor real-world performance metrics
## References
- React Performance: https://react.dev/learn/render-and-commit
- React Profiler: https://react.dev/reference/react/Profiler
- React Compiler: https://react.dev/reference/react-compiler
- Web Vitals: https://web.dev/vitals/