Optimizing React Performance: A Practical Guide
Optimizing React Performance: A Practical Guide
As React applications grow, performance can become a critical concern. In this guide, I'll share practical techniques I've used in production applications at Deriv to significantly improve performance.
Understanding React's Rendering Behavior
Before diving into optimization techniques, it's crucial to understand how React renders components. React re-renders a component when:
- State changes
- Props change
- Parent component re-renders
- Context value changes
Key Optimization Techniques
1. Code Splitting with React.lazy()
One of the most effective ways to improve initial load time is code splitting:
import { lazy, Suspense } from 'react';
// Instead of regular imports
const Dashboard = lazy(() => import('./Dashboard'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
);
}
Real-world impact: At Deriv, we reduced our initial bundle size by 40% using route-based code splitting.
2. Memoization with useMemo and useCallback
Prevent expensive calculations and unnecessary re-renders:
import { useMemo, useCallback } from 'react';
function ProductList({ items, onSelect }) {
// Memoize expensive calculations
const totalPrice = useMemo(() => {
return items.reduce((sum, item) => sum + item.price, 0);
}, [items]);
// Memoize callback functions
const handleClick = useCallback((id) => {
onSelect(id);
}, [onSelect]);
return (
<div>
<p>Total: ${totalPrice}</p>
{items.map(item => (
<Product key={item.id} onClick={() => handleClick(item.id)} />
))}
</div>
);
}
3. React.memo for Component Memoization
Prevent re-renders when props haven't changed:
import { memo } from 'react';
const ExpensiveComponent = memo(({ data }) => {
// Complex rendering logic
return <div>{/* ... */}</div>;
}, (prevProps, nextProps) => {
// Custom comparison function
return prevProps.data.id === nextProps.data.id;
});
4. Virtualization for Long Lists
Use libraries like react-window for 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={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
Result: Rendering 10,000+ items went from 2000ms to 50ms.
Performance Monitoring
Using React DevTools Profiler
The Profiler API helps identify performance bottlenecks:
import { Profiler } from 'react';
function onRenderCallback(id, phase, actualDuration) {
console.log(`${id} (${phase}) took ${actualDuration}ms`);
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<MyComponent />
</Profiler>
);
}
Common Anti-Patterns to Avoid
❌ Creating Objects/Arrays in JSX
// Bad - creates new object on every render
<Component style={{ margin: 10 }} />
// Good - memoize or move outside component
const style = { margin: 10 };
<Component style={style} />
❌ Inline Arrow Functions in Props
// Bad - creates new function on every render
<Button onClick={() => handleClick(id)} />
// Good - use useCallback or bind
const handleClickMemo = useCallback(() => handleClick(id), [id]);
<Button onClick={handleClickMemo} />
Measuring Success
After implementing these optimizations, we saw:
- 85% reduction in initial bundle size
- 60% improvement in Time to Interactive (TTI)
- 40% faster navigation between routes
- 20% increase in mobile conversion rates
Conclusion
Performance optimization is an ongoing process. Start by measuring, identify bottlenecks using React DevTools Profiler, and apply targeted optimizations. Remember: premature optimization is the root of all evil—only optimize what matters to your users.
Key Takeaways
- Use code splitting for route-based components
- Memoize expensive calculations and callbacks
- Virtualize long lists
- Avoid common anti-patterns
- Always measure before and after
Have questions or want to discuss React performance? Feel free to reach out!