Introduction
React applications can become slow as they grow. Understanding performance optimization techniques helps you build fast, responsive applications. This guide covers essential React performance strategies.
Understanding React Rendering
How React Renders
React creates a virtual DOM and compares changes before updating the real DOM. Excessive or unnecessary rendering causes performance issues.
When Components Re-render
- Parent re-renders
- State changes
- Context changes
- Props change
Memoization
React.memo
Memoizes components to prevent unnecessary re-renders:
const Button = React.memo(({ onClick, children }) => {
return <button onClick={onClick}>{children}</button>;
});
useMemo
Memoizes expensive calculations:
function ExpensiveComponent({ data }) {
const processed = useMemo(() => {
return data.map(item => ({
...item,
value: heavyComputation(item.value)
}));
}, [data]);
return <List items={processed} />;
}
useCallback
Memoizes functions to prevent unnecessary re-renders:
function ParentComponent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
return <Child onClick={handleClick} />;
}
Code Splitting
Dynamic Imports
const LazyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
);
}
Route-Based Splitting
import { BrowserRouter, Routes, Route } from 'react-router-dom';
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Suspense fallback={<Loading/>}><Dashboard/></Suspense>} />
<Route path="/settings" element={<Suspense fallback={<Loading/>}><Settings/></Suspense>} />
</Routes>
);
}
Virtualization
Rendering Large Lists
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>{items[index].name}</div>
);
return (
<FixedSizeList
height={400}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
Libraries
- react-window: Lightweight virtualization
- react-virtualized: More features
- tanstack virtual: Modern, flexible
Optimization Tools
React DevTools Profiler
import { Profiler } from 'react';
function onRender(id, phase, actualDuration) {
console.log(`${id} ${phase}: ${actualDuration}ms`);
}
<Profiler id="MyComponent" onRender={onRender}>
<MyComponent />
</Profiler>
useWhyDidYouUpdate
function useWhyDidYouUpdate(name, props) {
const previousProps = useRef(props);
useEffect(() => {
const keys = Object.keys(props);
const changes = keys.filter(key =>
props[key] !== previousProps.current[key]
);
if (changes.length > 0) {
console.log(`${name} changed:`, changes);
}
previousProps.current = props;
});
}
State Management
Optimizing Context
// Split contexts to avoid unnecessary re-renders
const UserContext = createContext();
const ThemeContext = createContext();
function App() {
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<Component />
</ThemeContext.Provider>
</UserContext.Provider>
);
}
State Colocation
// Bad: State at the top
function App() {
const [count, setCount] = useState(0);
const [theme, setTheme] = useState('dark');
return <Everything count={count} theme={theme} />;
}
// Good: State co-located
function Counter() {
const [count, setCount] = useState(0);
return <Display count={count} onIncrement={() => setCount(c => c + 1)} />;
}
function ThemeToggle() {
const [theme, setTheme] = useState('dark');
return <Button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')} />;
}
Bundle Optimization
Analyzing Bundles
# Webpack bundle analyzer
npm install --save-dev webpack-bundle-analyzer
# Next.js
next build --analyze
Reducing Bundle Size
// Instead of lodash
import { cloneDeep, merge } from 'lodash-es';
// Use specific imports
import cloneDeep from 'lodash/cloneDeep';
// Or use alternatives
import { pick } from 'lodash';
// vs
import pick from 'lodash/pick';
Performance Patterns
Callback Patterns
// Stable callbacks
const handleClick = useEventCallback((id) => {
// Handle click
});
// Custom hook for event callbacks
function useEventCallback(fn, dependencies) {
const ref = useRef(() => {
throw new Error('Cannot call event handler while rendering');
});
useEffect(() => {
ref.current = fn;
}, [fn, ...dependencies]);
return useCallback((...args) => {
ref.current(...args);
}, [ref]);
}
Deferred Values
function SearchResults({ query }) {
const [deferredQuery, setDeferredQuery] = useState(query);
useEffect(() => {
setDeferredQuery(query);
}, [query]);
// DeferredValue ensures typing doesn't block rendering
const deferred = useDeferredValue(deferredQuery);
return <SlowList query={deferred} />;
}
Measuring Performance
Core Web Vitals
- LCP: Largest Contentful Paint
- FID: First Input Delay
- CLS: Cumulative Layout Shift
Custom Metrics
function measureRenderTime(name, Component) {
return function MeasuredComponent(props) {
const start = performance.now();
const result = renderToReactTree(<Component {...props} />);
const end = performance.now();
console.log(`${name} took ${end - start}ms`);
return result;
};
}
Common Mistakes
Over-Memoization
Not everything needs memoization:
// Unnecessary - simple calculations
const total = a + b;
// Only memoize expensive operations
const expensive = useMemo(() => {
return heavyCalculation(data);
}, [data]);
Premature Optimization
Profile first, optimize second:
- Identify the bottleneck
- Measure the impact
- Optimize strategically
Conclusion
Performance optimization requires understanding React’s rendering model. Use profiling tools, identify real bottlenecks, and apply targeted optimizations. Remember: don’t optimize prematurely.
Comments