ReactTypeScriptCoding
React Performance Patterns: From Memo to Virtualization
Ihor Chyshkala
Performance optimization in React applications requires a deep understanding of rendering behavior and various optimization techniques. In this article, we'll explore different performance patterns, from basic memoization to advanced virtualization techniques.
Understanding React's Rendering Behavior
Before diving into optimization patterns, let's understand when and why React components re-render:
function ParentComponent() {
const [count, setCount] = useState(0);
return (
<div>
<Counter count={count} />
<ExpensiveComponent /> {/* Re-renders on every count change */}
</div>
);
}
function ExpensiveComponent() {
// Expensive calculations or rendering
return <div>{/* ... */}</div>;
}
Memoization Patterns
Using React.memo
// Before optimization
function MovieCard({ title, rating, onFavorite }: MovieCardProps) {
return (
<div className="card">
<h3>{title}</h3>
<div>{rating} stars</div>
<button onClick={onFavorite}>Favorite</button>
</div>
);
}
// After optimization
const MovieCard = React.memo(
function MovieCard({ title, rating, onFavorite }: MovieCardProps) {
return (
<div className="card">
<h3>{title}</h3>
<div>{rating} stars</div>
<button onClick={onFavorite}>Favorite</button>
</div>
);
},
(prevProps, nextProps) => {
return (
prevProps.title === nextProps.title &&
prevProps.rating === nextProps.rating
);
}
);
Optimizing with useMemo
function SearchResults({ items, query }: SearchResultsProps) {
const filteredItems = useMemo(() => {
console.log('Filtering items...'); // Expensive operation
return items.filter(item =>
item.title.toLowerCase().includes(query.toLowerCase())
);
}, [items, query]);
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
}
useCallback for Event Handlers
function ProductList({ products }: ProductListProps) {
const [favorites, setFavorites] = useState<Set<string>>(new Set());
const handleFavorite = useCallback((productId: string) => {
setFavorites(prev => {
const next = new Set(prev);
if (next.has(productId)) {
next.delete(productId);
} else {
next.add(productId);
}
return next;
});
}, []);
return (
<div>
{products.map(product => (
<MovieCard
key={product.id}
{...product}
onFavorite={() => handleFavorite(product.id)}
/>
))}
</div>
);
}
Debouncing and Throttling
Custom Debounce Hook
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
// Usage in search component
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearch = useDebounce(searchTerm, 300);
useEffect(() => {
// API call here
console.log('Searching for:', debouncedSearch);
}, [debouncedSearch]);
return (
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
);
}
Custom Throttle Hook
function useThrottle<T>(value: T, interval: number): T {
const [throttledValue, setThrottledValue] = useState(value);
const lastExecuted = useRef<number>(Date.now());
useEffect(() => {
const handler = setTimeout(() => {
const now = Date.now();
if (now >= lastExecuted.current + interval) {
setThrottledValue(value);
lastExecuted.current = now;
}
}, interval);
return () => {
clearTimeout(handler);
};
}, [value, interval]);
return throttledValue;
}
Virtualization Techniques
Basic Virtualization Hook
interface UseVirtualizationProps {
itemCount: number;
itemHeight: number;
containerHeight: number;
overscan?: number;
}
function useVirtualization({
itemCount,
itemHeight,
containerHeight,
overscan = 3
}: UseVirtualizationProps) {
const [scrollTop, setScrollTop] = useState(0);
const visibleItems = useMemo(() => {
const startIndex = Math.max(
0,
Math.floor(scrollTop / itemHeight) - overscan
);
const endIndex = Math.min(
itemCount - 1,
Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
);
return {
startIndex,
endIndex,
items: Array.from(
{ length: endIndex - startIndex + 1 },
(_, index) => startIndex + index
),
offsetY: startIndex * itemHeight
};
}, [scrollTop, itemHeight, containerHeight, itemCount, overscan]);
return {
visibleItems,
onScroll: (e: React.UIEvent<HTMLDivElement>) => {
setScrollTop(e.currentTarget.scrollTop);
}
};
}
// Usage
function VirtualizedList({ items }: { items: string[] }) {
const {
visibleItems,
onScroll
} = useVirtualization({
itemCount: items.length,
itemHeight: 40,
containerHeight: 400
});
return (
<div
style={{ height: '400px', overflow: 'auto' }}
onScroll={onScroll}
>
<div style={{ height: `${items.length * 40}px`, position: 'relative' }}>
{visibleItems.items.map(index => (
<div
key={index}
style={{
position: 'absolute',
top: `${index * 40}px`,
height: '40px',
width: '100%'
}}
>
{items[index]}
</div>
))}
</div>
</div>
);
}
Code Splitting and Lazy Loading
Route-Based Code Splitting
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
Component-Based Code Splitting
const HeavyChart = lazy(() => import('./components/HeavyChart'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && (
<Suspense fallback={<LoadingSpinner />}>
<HeavyChart />
</Suspense>
)}
</div>
);
}
Performance Monitoring
Custom Performance Hook
function usePerformanceMonitor(componentName: string) {
const renderCount = useRef(0);
const lastRenderTime = useRef(performance.now());
useEffect(() => {
const renderTime = performance.now() - lastRenderTime.current;
renderCount.current += 1;
console.log(`${componentName} render #${renderCount.current} took ${renderTime}ms`);
lastRenderTime.current = performance.now();
});
}
// Usage
function ExpensiveComponent() {
usePerformanceMonitor('ExpensiveComponent');
// ... component logic
}
Measuring Re-Renders
Best Practices and Guidelines
- When to Use Memoization
- For expensive calculations
- For preventing unnecessary re-renders
- When props are stable
// Good use case
const expensiveValue = useMemo(() => {
return someExpensiveCalculation(props.data);
}, [props.data]);
// Bad use case (over-optimization)
const simpleValue = useMemo(() => {
return props.value + 1;
}, [props.value]);
2. Virtualization Considerations
- Use for large lists (100+ items)
- Consider variable height items
- Handle scroll restoration
3. Code Splitting Guidelines
- Split by route
- Split by feature
- Split by viewport visibility
4. Performance Testing
describe('Performance Tests', () => {
it('should render list efficiently', async () => {
const startTime = performance.now();
render(<VirtualizedList items={largeDataSet} />);
const endTime = performance.now();
expect(endTime - startTime).toBeLessThan(100);
});
});
Conclusion
React performance optimization is a balance between:
- Code complexity
- Bundle size
- Runtime performance
- Development experience
Key takeaways:
- Use memoization judiciously
- Implement virtualization for large lists
- Split code based on user needs
- Monitor and measure performance
- Focus on user-perceived performance