In this comprehensive lesson, you'll master performance optimization techniques for React applications. You'll learn how to identify performance bottlenecks, implement optimization strategies, and ensure your applications run smoothly and efficiently. From component-level optimizations to application-wide performance patterns, you'll build the skills to create fast, responsive React applications.
React applications can face performance issues due to:
Unnecessary Re-renders: Components re-rendering when props haven't changed Expensive Computations: Heavy calculations running on every render Large Bundle Sizes: JavaScript bundles that take too long to download Inefficient State Updates: State changes causing cascading re-renders Memory Leaks: Components not cleaning up resources properly
Performance Philosophy: Optimize based on measurements, not assumptions. Focus on user-perceived performance improvements.
React's rendering process can be optimized at multiple levels:
Component Level: Prevent unnecessary re-renders with memoization Application Level: Optimize bundle size and loading performance User Experience: Focus on perceived performance and responsiveness
Key Metrics: First Contentful Paint (FCP), Time to Interactive (TTI), Cumulative Layout Shift (CLS)
React.memo prevents component re-renders when props haven't changed:
// Without memoization - re-renders on every parent render
function ExpensiveComponent({ data, onClick }) {
console.log('ExpensiveComponent rendered')
return (
<div>
{data.map(item => <div key={item.id}>{item.name}</div>)}
<button onClick={onClick}>Click</button>
</div>
)
}
// With memoization - only re-renders when props change
const MemoizedExpensiveComponent = React.memo(function ExpensiveComponent({ data, onClick }) {
console.log('ExpensiveComponent rendered')
return (
<div>
{data.map(item => <div key={item.id}>{item.name}</div>)}
<button onClick={onClick}>Click</button>
</div>
)
})
// Custom comparison function
const CustomMemoizedComponent = React.memo(function Component({ data, onClick }) {
return <div>{/* component content */}</div>
}, (prevProps, nextProps) => {
// Custom comparison logic
return prevProps.data.length === nextProps.data.length
})
useMemo caches expensive calculations between renders:
function ExpensiveCalculation({ items, filter }) {
// Expensive filtering and sorting
const filteredAndSortedItems = useMemo(() => {
console.log('Expensive calculation running')
return items
.filter(item => item.category === filter)
.sort((a, b) => a.name.localeCompare(b.name))
}, [items, filter]) // Only re-calculate when dependencies change
return (
<div>
{filteredAndSortedItems.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
)
}
// Complex calculation example
function Dashboard({ data, userPreferences }) {
const analytics = useMemo(() => {
// Complex analytics calculation
const total = data.reduce((sum, item) => sum + item.value, 0)
const average = total / data.length
const distribution = data.reduce((acc, item) => {
acc[item.category] = (acc[item.category] || 0) + 1
return acc
}, {})
return { total, average, distribution }
}, [data])
return <AnalyticsDisplay data={analytics} />
}
useMemo memoizes functions to prevent child re-renders:
function ParentComponent() {
const [count, setCount] = useState(0)
// Without useCallback - new function on every render
const handleClick = () => {
setCount(count + 1)
}
// With useCallback - same function reference
const memoizedHandleClick = useCallback(() => {
setCount(prev => prev + 1)
}, []) // Empty dependency array
return (
<div>
<ChildComponent onClick={memoizedHandleClick} />
<div>Count: {count}</div>
</div>
)
}
// Child component that benefits from memoized callback
const ChildComponent = React.memo(function ChildComponent({ onClick }) {
console.log('Child rendered')
return <button onClick={onClick}>Increment</button>
})
Code splitting reduces initial bundle size by loading code on demand:
// Route-based code splitting
import { lazy, Suspense } from 'react'
const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</Router>
)
}
// Component-based code splitting
function LazyComponent() {
const [HeavyComponent, setHeavyComponent] = useState(null)
useEffect(() => {
import('./HeavyComponent').then(module => {
setHeavyComponent(() => module.default)
})
}, [])
return HeavyComponent ? <HeavyComponent /> : <div>Loading...</div>
}
// Conditional code splitting
function AdminPanel({ user }) {
const AdminTools = useMemo(() =>
lazy(() => import('./AdminTools')), []
)
if (!user.isAdmin) {
return <div>Access denied</div>
}
return (
<Suspense fallback={<div>Loading admin tools...</div>}>
<AdminTools />
</Suspense>
)
}
// Preloading components
function usePreloadComponent(importFunction) {
const [component, setComponent] = useState(null)
useEffect(() => {
importFunction().then(module => {
setComponent(() => module.default)
})
}, [importFunction])
return component
}
// Usage
const ChartComponent = usePreloadComponent(() => import('./Chart'))
Optimize your JavaScript bundles for better performance:
// webpack-bundle-analyzer configuration
{
"scripts": {
"analyze": "npm run build && npx webpack-bundle-analyzer .next/static/chunks/"
}
}
// Next.js optimization configuration
// next.config.js
module.exports = {
webpack: (config, { isServer }) => {
if (!isServer) {
config.optimization.splitChunks = {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
}
}
return config
},
compress: true,
poweredByHeader: false
}
// Good: Import only what you need
import { Button, Input } from '@/components/ui'
import { formatCurrency } from '@/utils/formatters'
// Bad: Import entire libraries
import * as UI from '@/components/ui'
import * as Utils from '@/utils'
// Dynamic imports for large libraries
function ChartComponent({ type }) {
const [chartLib, setChartLib] = useState(null)
useEffect(() => {
const loadChart = async () => {
if (type === 'line') {
const lib = await import('chart.js/Chart.Line')
setChartLib(lib.default)
} else if (type === 'bar') {
const lib = await import('chart.js/Chart.Bar')
setChartLib(lib.default)
}
}
loadChart()
}, [type])
return chartLib ? <chartLib data={data} /> : <div>Loading...</div>
}
Virtualization renders only visible items for large datasets:
// Simple virtual list implementation
function VirtualList({ items, itemHeight, containerHeight }) {
const [scrollTop, setScrollTop] = useState(0)
const visibleStart = Math.floor(scrollTop / itemHeight)
const visibleEnd = Math.min(
visibleStart + Math.ceil(containerHeight / itemHeight),
items.length
)
const visibleItems = items.slice(visibleStart, visibleEnd)
return (
<div
style={{ height: containerHeight, overflow: 'auto' }}
onScroll={(e) => setScrollTop(e.target.scrollTop)}
>
<div style={{ height: items.length * itemHeight, position: 'relative' }}>
{visibleItems.map((item, index) => (
<div
key={item.id}
style={{
position: 'absolute',
top: (visibleStart + index) * itemHeight,
height: itemHeight,
width: '100%',
}}
>
{item.content}
</div>
))}
</div>
</div>
)
}
// Usage with react-window
import { FixedSizeList as List } from 'react-window'
function OptimizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].content}
</div>
)
return (
<List
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</List>
)
}
// Lazy loading images
function LazyImage({ src, alt, ...props }) {
const [isLoaded, setIsLoaded] = useState(false)
const [isInView, setIsInView] = useState(false)
const imgRef = useRef()
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true)
observer.disconnect()
}
},
{ threshold: 0.1 }
)
if (imgRef.current) {
observer.observe(imgRef.current)
}
return () => observer.disconnect()
}, [])
return (
<div ref={imgRef} {...props}>
{isInView && (
<img
src={src}
alt={alt}
onLoad={() => setIsLoaded(true)}
style={{ opacity: isLoaded ? 1 : 0 }}
/>
)}
</div>
)
}
// Responsive images
function ResponsiveImage({ src, alt, sizes }) {
const [currentSrc, setCurrentSrc] = useState('')
useEffect(() => {
const updateImage = () => {
const width = window.innerWidth
let selectedSrc = src.default
if (width >= 1024 && src.large) {
selectedSrc = src.large
} else if (width >= 768 && src.medium) {
selectedSrc = src.medium
}
setCurrentSrc(selectedSrc)
}
updateImage()
window.addEventListener('resize', updateImage)
return () => window.removeEventListener('resize', updateImage)
}, [src])
return <img src={currentSrc} alt={alt} />
}
Monitor component performance with React Profiler:
// Performance monitoring component
function PerformanceProfiler({ children, id }) {
const onRender = (id, phase, actualDuration) => {
if (actualDuration > 16) { // More than one frame
console.warn(`Slow render detected in ${id}:`, {
phase,
duration: actualDuration,
timestamp: performance.now()
})
}
}
return (
<React.Profiler id={id} onRender={onRender}>
{children}
</React.Profiler>
)
}
// Usage
function App() {
return (
<PerformanceProfiler id="App">
<Header />
<Main />
<Footer />
</PerformanceProfiler>
)
}
// Performance measurement hook
function usePerformanceMonitor(componentName) {
const renderCount = useRef(0)
const lastRenderTime = useRef(performance.now())
useEffect(() => {
renderCount.current += 1
const now = performance.now()
const timeSinceLastRender = now - lastRenderTime.current
console.log(`${componentName} render #${renderCount.current}`, {
timeSinceLastRender,
timestamp: now
})
lastRenderTime.current = now
})
return renderCount.current
}
// Usage
function ExpensiveComponent({ data }) {
const renderCount = usePerformanceMonitor('ExpensiveComponent')
return (
<div>
Renders: {renderCount}
{data.map(item => <div key={item.id}>{item.name}</div>)}
</div>
)
}
// Avoid state in high-frequency components
function BadCounter() {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
</div>
)
}
// Use refs for non-rendering values
function GoodCounter() {
const countRef = useRef(0)
const [displayCount, setDisplayCount] = useState(0)
const handleClick = () => {
countRef.current += 1
// Only update state when necessary
if (countRef.current % 10 === 0) {
setDisplayCount(countRef.current)
}
}
return (
<div>
<button onClick={handleClick}>
Clicks: {countRef.current} (Display: {displayCount})
</button>
</div>
)
}
// Avoid layout thrashing
function BadLayout() {
const items = Array.from({ length: 1000 }, (_, i) => i)
return (
<div>
{items.map(item => (
<div
key={item}
style={{
position: 'absolute',
left: Math.random() * window.innerWidth,
top: Math.random() * window.innerHeight,
}}
>
{item}
</div>
))}
</div>
)
}
// Use CSS transforms for better performance
function GoodLayout() {
const items = Array.from({ length: 1000 }, (_, i) => i)
return (
<div>
{items.map(item => (
<div
key={item}
style={{
transform: `translate(${Math.random() * window.innerWidth}px, ${Math.random() * window.innerHeight}px)`,
}}
>
{item}
</div>
))}
</div>
)
}
In this comprehensive lesson, you've mastered performance optimization techniques for React applications. You now understand:
In the next lesson, you'll learn about deployment and DevOps practices, ensuring your optimized applications reach production efficiently and reliably.