React is fast by default, but as your application grows, performance becomes important. Unnecessary re-renders, large bundle sizes, and unoptimized code can slow things down. In this lesson, you'll learn how to identify and fix performance bottlenecks using React.memo, lazy loading, code splitting, and other optimization techniques. Let's make your React apps blazing fast! ⚡
When to Optimize
Premature Optimization Warning:
"Premature optimization is the root of all evil" - Donald Knuth
Don't optimize until you have a problem!
The right approach:
1. Build your app normally
2. Measure performance (React DevTools Profiler)
3. Find actual bottlenecks
4. Then optimize those specific areas
Optimization adds complexity - only do it when needed!
Signs You Need to Optimize
- Slow interactions (typing, clicking)
- Laggy scrolling
- Slow page loads
- High CPU usage in dev tools
- Many unnecessary re-renders
- Large bundle size (slow initial load)
React.memo - Preventing Re-renders
React.memo prevents components from re-rendering when props haven't changed:
The Problem: Unnecessary Re-renders
function Parent() {
const [count, setCount] = React.useState(0)
const [name, setName] = React.useState('Alice')
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
{/* ❌ Problem: ExpensiveChild re-renders when name changes */}
<ExpensiveChild count={count} />
</div>
)
}
function ExpensiveChild({ count }) {
console.log('ExpensiveChild rendered')
// Expensive calculation
const result = expensiveCalculation(count)
return <div>Result: {result}</div>
}
// When you type in input:
// 1. name changes
// 2. Parent re-renders
// 3. ExpensiveChild re-renders (even though count didn't change!)
// 4. Expensive calculation runs again (wasteful!)The Solution: React.memo
function Parent() {
const [count, setCount] = React.useState(0)
const [name, setName] = React.useState('Alice')
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
{/* ✅ Solution: MemoizedChild only re-renders when count changes */}
<MemoizedChild count={count} />
</div>
)
}
// Wrap component in React.memo
const MemoizedChild = React.memo(function ExpensiveChild({ count }) {
console.log('ExpensiveChild rendered')
const result = expensiveCalculation(count)
return <div>Result: {result}</div>
})
// Now when you type:
// 1. name changes
// 2. Parent re-renders
// 3. React.memo checks: did props change?
// 4. count is same → skip re-render!
// 5. Expensive calculation not run (fast!)React.memo with Custom Comparison
// Default: Shallow comparison of props
const MemoizedComponent = React.memo(Component)
// Custom comparison function
const MemoizedComponent = React.memo(Component, (prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
// Return false if props changed (do re-render)
return prevProps.id === nextProps.id &&
prevProps.name === nextProps.name
})
// Example: Only re-render if user ID changes
const UserProfile = React.memo(
function UserProfile({ user, theme }) {
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
},
(prevProps, nextProps) => {
// Only compare user.id, ignore theme changes
return prevProps.user.id === nextProps.user.id
}
)
// ⚠️ Careful: Custom comparison can be expensive too!
// Only use when default comparison isn't enoughWhen to Use React.memo:
- DO use when:
- • Component renders often with same props
- • Component is expensive to render
- • Component is in a list
- • Profiler shows unnecessary re-renders
- DON'T use when:
- • Props change frequently
- • Component is simple/fast
- • No performance issue exists
Lazy Loading and Code Splitting
Load components only when needed, reducing initial bundle size:
Basic Lazy Loading
import React, { Suspense, lazy } from 'react'
// ❌ Regular import - Loaded immediately
import Dashboard from './Dashboard'
// ✅ Lazy import - Loaded only when needed
const Dashboard = lazy(() => import('./Dashboard'))
function App() {
const [showDashboard, setShowDashboard] = React.useState(false)
return (
<div>
<button onClick={() => setShowDashboard(true)}>
Show Dashboard
</button>
{showDashboard && (
<Suspense fallback={<div>Loading...</div>}>
<Dashboard />
</Suspense>
)}
</div>
)
}
// How it works:
// 1. Dashboard code not in initial bundle
// 2. User clicks button
// 3. Dashboard code downloaded
// 4. Loading... shown while downloading
// 5. Dashboard rendered when ready
// Benefits:
// ✅ Smaller initial bundle
// ✅ Faster initial load
// ✅ Better user experienceLazy Loading Routes
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
// Lazy load page components
const Home = lazy(() => import('./pages/HomePage'))
const About = lazy(() => import('./pages/AboutPage'))
const Dashboard = lazy(() => import('./pages/DashboardPage'))
const Profile = lazy(() => import('./pages/ProfilePage'))
// Loading component
function LoadingSpinner() {
return (
<div className="loading">
<div className="spinner"></div>
<p>Loading...</p>
</div>
)
}
function App() {
return (
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
</BrowserRouter>
)
}
// Each route is a separate chunk
// Only loads when user navigates to it!
// Bundle results:
// main.js - Core app (small)
// home.js - Home page
// about.js - About page
// dashboard.js - Dashboard page
// profile.js - Profile pageNamed Exports with Lazy
// Component file with named export
// UserProfile.jsx
export function UserProfile({ userId }) {
return <div>User {userId}</div>
}
// ❌ Won't work - lazy needs default export
const UserProfile = lazy(() => import('./UserProfile'))
// ✅ Solution 1: Change to default export
// UserProfile.jsx
export default function UserProfile({ userId }) {
return <div>User {userId}</div>
}
// ✅ Solution 2: Create wrapper with default export
const UserProfile = lazy(() =>
import('./UserProfile').then(module => ({
default: module.UserProfile
}))
)
// ✅ Solution 3: Re-export as default
// UserProfile.jsx
export function UserProfile({ userId }) {
return <div>User {userId}</div>
}
export { UserProfile as default }
// Lazy loading requires default exports!Interactive Demo
💡 Tip: Edit the code above and click "Run" to see your changes
Bundle Size Optimization
1. Analyze Bundle Size
# Install bundle analyzer
npm install --save-dev webpack-bundle-analyzer
# For Create React App
npm install --save-dev cra-bundle-analyzer
# Build and analyze
npm run build
npx cra-bundle-analyzer
# Shows visual representation of bundle contents
# Identify large dependencies to optimize2. Import Only What You Need
// ❌ Bad - Imports entire library
import _ from 'lodash'
const result = _.debounce(fn, 300)
// ✅ Good - Imports only debounce
import debounce from 'lodash/debounce'
const result = debounce(fn, 300)
// ❌ Bad - Imports all icons
import * as Icons from 'react-icons/fa'
<Icons.FaUser />
// ✅ Good - Imports specific icon
import { FaUser } from 'react-icons/fa'
<FaUser />
// ❌ Bad - Imports entire Material-UI
import { Button, TextField, Modal } from '@mui/material'
// ✅ Good - Direct imports
import Button from '@mui/material/Button'
import TextField from '@mui/material/TextField'
import Modal from '@mui/material/Modal'
// Tree-shaking removes unused code
// Only works with ES6 imports!3. Remove Unused Dependencies
# Find unused dependencies
npx depcheck
# Remove unused packages
npm uninstall unused-package
# Check for duplicate dependencies
npm dedupe
# Regularly audit dependencies
npm auditOptimizing Lists
Always Use Keys
// ❌ Bad - No key or index as key
users.map(user => <UserCard user={user} />)
users.map((user, index) => <UserCard key={index} user={user} />)
// ✅ Good - Unique, stable ID as key
users.map(user => <UserCard key={user.id} user={user} />)
// Why keys matter:
// - React uses keys to track items
// - Stable keys = efficient updates
// - Index as key = bad performance on reorder/filterVirtual Scrolling for Large Lists
// For lists with 1000+ items, use virtualization
// Library: react-window or react-virtualized
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>
)
}
// Only renders visible items!
// Handles 10,000+ items smoothlyProfiling React Apps
React DevTools Profiler
Steps to profile:
1. Install React DevTools browser extension
2. Open DevTools → Profiler tab
3. Click record button (circle)
4. Interact with your app
5. Click stop button
6. Analyze results:
- Flame graph shows component hierarchy
- Ranked chart shows slowest components
- Click components to see render details
What to look for:
- Components that render frequently
- Long render times
- Unnecessary re-renders
- Large component trees
Color coding:
- Gray: Did not render
- Green: Fast render
- Yellow: Medium render
- Red: Slow render (optimize these!)Programmatic Profiling
import { Profiler } from 'react'
function onRenderCallback(
id, // Component identifier
phase, // "mount" or "update"
actualDuration, // Time to render
baseDuration, // Estimated time without memoization
startTime, // When render started
commitTime, // When render committed
interactions // Set of interactions
) {
console.log(`${id} took ${actualDuration}ms to render`)
// Send to analytics
if (actualDuration > 100) {
analytics.track('slow-render', {
component: id,
duration: actualDuration
})
}
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Dashboard />
<Sidebar />
<Content />
</Profiler>
)
}
// Wrap components to measure performance
// Production-safe, minimal overheadPerformance Optimization Checklist
Before Optimizing:
- ✓ Profile with React DevTools
- ✓ Identify actual bottlenecks
- ✓ Measure current performance
- ✓ Set target metrics
Component Optimization:
- ✓ Use React.memo for expensive components
- ✓ Use useMemo for expensive calculations
- ✓ Use useCallback for callback props
- ✓ Avoid inline objects/functions in JSX
- ✓ Split large components into smaller ones
Bundle Optimization:
- ✓ Lazy load routes
- ✓ Code split large features
- ✓ Tree-shake unused code
- ✓ Analyze bundle size
- ✓ Remove unused dependencies
List Optimization:
- ✓ Use stable keys
- ✓ Virtualize long lists
- ✓ Paginate large datasets
- ✓ Debounce search/filter
After Optimizing:
- ✓ Measure improvement
- ✓ Profile again
- ✓ Test in production build
- ✓ Monitor in production
Key Takeaways
- Don't optimize prematurely - measure first!
- Use React DevTools Profiler to find bottlenecks
React.memoprevents unnecessary re-renders- Lazy loading reduces initial bundle size
- Code splitting loads components on demand
Suspenseshows fallback while loading- Import only what you need for tree-shaking
- Use stable keys for lists
- Virtualize lists with 1000+ items
- Combine with
useMemoanduseCallback - Analyze bundle size regularly
- Remove unused dependencies
- Test performance in production build
What's Next?
You now know how to optimize React applications for performance! You understand React.memo, lazy loading, code splitting, and profiling.
In the next lesson, you'll learn about Error Handling:
- Error boundaries for catching errors
- Graceful error handling strategies
- Error recovery patterns
- Logging and monitoring errors
- User-friendly error messages
Get ready to build robust React applications that handle errors gracefully! 🛡️