Optimizing Performance with useMemo
A common React performance pitfall is not memoizing expensive computations, which can lead to unnecessary re-calculations and sluggish user interfaces, especially in complex components.
The Problem: Unmemoized Computations
// ❌ Expensive computation runs on every render
function ProductList({ products, filters }) {
const filteredProducts = products.filter(p =>
p.price >= filters.minPrice &&
p.category === filters.category
);
return (
<ul>
{filteredProducts.map(product => (
<ProductItem key={product.id} product={product} />
))}
</ul>
);
}
Why This is Problematic
- Wasted CPU Cycles: Recalculating on every render
- Performance Bottlenecks: Especially with large datasets
- Unnecessary Child Renders: Downstream components re-render
- Battery Drain: On mobile devices
Correct Implementation with useMemo
1. Basic Memoization
function ProductList({ products, filters }) {
const filteredProducts = useMemo(() => {
return products.filter(p =>
p.price >= filters.minPrice &&
p.category === filters.category
);
}, [products, filters.minPrice, filters.category]);
return (
<ul>
{filteredProducts.map(product => (
<ProductItem key={product.id} product={product} />
))}
</ul>
);
}
2. Complex Object Derivation
function Dashboard({ users, config }) {
const analyticsData = useMemo(() => {
return {
activeUsers: users.filter(u => u.isActive).length,
premiumUsers: users.filter(u => u.isPremium).length,
sortedUsers: [...users].sort((a, b) =>
config.sortAsc ? a.score - b.score : b.score - a.score
)
};
}, [users, config.sortAsc]);
return <AnalyticsChart data={analyticsData} />;
}
3. With External Dependencies
function DataVisualization({ rawData }) {
const processedData = useMemo(() => {
return transformData(
rawData,
calculateStats,
applyNormalization
);
}, [rawData]); // Assuming transform functions are stable
return <Chart data={processedData} />;
}
When to Use useMemo
- Expensive Calculations: Sorting, filtering, transforming large datasets
- Stable References: When passing objects/arrays to optimized children
- Derived State: Complex state derivations
- Component Initialization: Heavy setup computations
When NOT to Use useMemo
- Simple Calculations: Basic math, small array operations
- Primitive Values: No need to memoize strings, numbers, booleans
- Every Render is Fast: If computation is cheap
- Dependencies Change Often: Memoization provides no benefit
Best Practices
- Profile First: Use React DevTools to identify bottlenecks
- Correct Dependencies: Include all values used in the computation
- Avoid Premature Optimization: Only optimize when needed
- Combine with useCallback: For stable function references
- Type Safety: With TypeScript, type the memoized value
const result = useMemo<ReturnType>(() => {...}, [deps]);
Common Pitfalls
- Missing Dependencies:
const filtered = useMemo(() => items.filter(/* uses count */), [items]);
// ❌ Missing count dependency
- Over-Memoization:
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
// ❌ Too simple to need memoization
- Impure Functions:
const data = useMemo(() => {
return items.map(item => ({ ...item, id: Math.random() }));
// ❌ Different result with same dependencies
}, [items]);
- Memory Leaks:
const heavyObject = useMemo(() => new HeavyClass(), []);
// ❌ Might retain memory unnecessarily
Advanced Patterns
1. Memoizing Component Props
function Parent({ items }) {
const itemProps = useMemo(() => ({
onClick: () => console.log('Item clicked'),
theme: 'dark'
}), []); // Stable reference
return items.map(item => (
<MemoizedChild key={item.id} item={item} {...itemProps} />
));
}
2. With useReducer
function reducer(state, action) {
switch (action.type) {
case 'SET_FILTER':
return { ...state, filter: action.payload };
// other cases
}
}
function ProductList({ products }) {
const [state, dispatch] = useReducer(reducer, initialState);
const filteredProducts = useMemo(() => {
return products.filter(p =>
p.price >= state.filter.minPrice
);
}, [products, state.filter.minPrice]);
}
3. Custom Comparison Function
import { useMemoCompare } from 'use-memo-compare';
function DataDisplay({ data }) {
const transformed = useMemoCompare(data, (prev, next) => {
return prev.id === next.id; // Custom comparison
});
}
Key Takeaways
- useMemo Caches Calculations: Between re-renders
- Dependency Array Matters: Like useEffect
- Not a Silver Bullet: Only for measurable performance issues
- Combine with React.memo: For complete optimization
- Measure Impact: Verify optimizations help with profiling
Remember: useMemo
is a performance optimization tool, not a semantic guarantee. React may choose to forget memoized values to free memory. Use it judiciously when you have measurable performance bottlenecks.