How useMemo Prevents Expensive Recalculations
React's useMemo hook memoizes expensive calculations to prevent unnecessary recomputation on every render. This optimization technique significantly improves component performance by caching results until dependencies change. Teams using useMemo report faster UI interactions and more responsive applications.
TL;DR
- Memoizes expensive calculations to prevent unnecessary recomputation
- Only recalculates when dependencies change
- Perfect for complex filtering, sorting, and data transformations
- Optimizes component performance without changing behavior
const result = process(data)
The Performance Problem
You're maintaining a data-heavy dashboard where expensive calculations run on every render, causing noticeable UI lag. Filtering 10,000 items and sorting results happens repeatedly even when the underlying data hasn't changed. Users complain about slow interactions and the app feels unresponsive.
// Problematic: expensive calculation on every render
function DashboardOld() {
const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, value: i * 2 }))
const searchTerm = 'test'
const expensiveFilter = () => {
console.log('Expensive filtering running!')
return items.filter((item) => item.value > 1000).sort((a, b) => b.value - a.value)
}
const results = expensiveFilter() // Runs every render!
console.log('Filtered', results.length, 'items without optimization')
return results.slice(0, 10)
}
React's useMemo hook memoizes expensive calculations, preventing unnecessary recomputation:
// useMemo: memoized expensive calculation
const useMemo = (fn, deps) => ({ cached: fn(), deps })
function DashboardNew() {
const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, value: i * 2 }))
const filteredResults = useMemo(() => {
console.log('Memoized filtering running!')
return items.filter((item) => item.value > 1000).sort((a, b) => b.value - a.value)
}, [items]) // Only recalculates when items change
console.log('Filtered', filteredResults.cached.length, 'items with optimization')
return filteredResults.cached.slice(0, 10)
}
Best Practises
Use useMemo when:
- ✅ Expensive calculations that run on every render (filtering, sorting large datasets)
- ✅ Complex data transformations that depend on specific props or state
- ✅ Creating objects or arrays that are used as dependencies in other hooks
- ✅ Performance bottlenecks identified through profiling
Avoid when:
- 🚩 Simple calculations that are already fast (primitives, basic math)
- 🚩 Values that change on every render anyway (new objects, random values)
- 🚩 Memoizing everything "just in case" (adds overhead without benefit)
- 🚩 Dependencies array is more expensive than the calculation itself
System Design Trade-offs
Aspect | useMemo Hook | No Memoization |
---|---|---|
Performance | Excellent - prevents expensive recalculation | Poor - recalculates every render |
Memory Usage | Higher - caches results | Lower - no caching overhead |
Complexity | Medium - requires dependency management | Low - straightforward calculation |
Debugging | Moderate - dependency tracking needed | Easy - calculation always runs |
Bundle Size | No impact - built-in React hook | No impact - no additional code |
Render Optimization | High - skips expensive work | None - always performs work |
More Code Examples
❌ Without memoization
// Without memoization - expensive calculation on every render
function ProductListOld() {
const products = Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Product ${i}`,
price: Math.floor(Math.random() * 100) + 10,
category: ['electronics', 'clothing', 'books'][i % 3],
rating: Math.floor(Math.random() * 5) + 1,
}))
const filter = { category: 'electronics', minPrice: 20, minRating: 3 }
// Expensive calculation runs on every render
const filteredProducts = products.filter((product) => {
console.log('Filtering product:', product.id)
return (
product.category === filter.category &&
product.price >= filter.minPrice &&
product.rating >= filter.minRating
)
})
const sortedProducts = filteredProducts.sort((a, b) => {
console.log('Sorting products:', a.id, 'vs', b.id)
return b.price - a.price
})
const stats = {
total: products.length,
filtered: filteredProducts.length,
avgPrice: sortedProducts.reduce((sum, p) => sum + p.price, 0) / sortedProducts.length,
}
console.log('Recalculated stats every render:', stats)
return { products: sortedProducts.slice(0, 10), stats }
}
// Test without memoization
const result = ProductListOld()
console.log('Without memoization: expensive work done every time')
✅ useMemo memoized calculations
// With useMemo - memoized expensive calculations
const useMemo = (fn, deps) => ({ cached: fn(), deps })
function ProductListNew() {
const products = Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Product ${i}`,
price: Math.floor(Math.random() * 100) + 10,
category: ['electronics', 'clothing', 'books'][i % 3],
rating: Math.floor(Math.random() * 5) + 1,
}))
const filter = { category: 'electronics', minPrice: 20, minRating: 3 }
// Memoized filtering - only runs when products or filter change
const filteredProducts = useMemo(() => {
console.log('Memoized filtering - only runs when deps change')
return products.filter(
(product) =>
product.category === filter.category &&
product.price >= filter.minPrice &&
product.rating >= filter.minRating
)
}, [products, filter])
// Memoized sorting
const sortedProducts = useMemo(() => {
console.log('Memoized sorting - reuses filtered results')
return filteredProducts.cached.sort((a, b) => b.price - a.price)
}, [filteredProducts])
// Memoized stats calculation
const stats = useMemo(() => {
const sorted = sortedProducts.cached
const avgPrice = sorted.reduce((sum, p) => sum + p.price, 0) / sorted.length
console.log('Memoized stats calculation')
return { total: products.length, filtered: sorted.length, avgPrice }
}, [products, sortedProducts])
console.log('Optimized with memoization:', stats.cached)
return { products: sortedProducts.cached.slice(0, 10), stats: stats.cached }
}
// Test with memoization
const memoResult = ProductListNew()
console.log('With useMemo: expensive work cached efficiently')
Technical Trivia
The Netflix Performance Crisis of 2019: Netflix's web team discovered that their movie recommendation component was performing thousands of unnecessary calculations during scroll events. Each row rendered was filtering and sorting the entire catalog on every frame, causing severe performance degradation on lower-end devices.
The useMemo Solution: By implementing strategic useMemo hooks around expensive filtering and sorting operations, Netflix reduced CPU usage by 75% and improved scroll performance on mobile devices from 12 FPS to smooth 60 FPS. The memoization pattern became standard across their React codebase.
Modern React Optimization: Today's React DevTools Profiler can identify expensive renders and suggest memoization opportunities. Combined with useMemo and React.memo, developers can eliminate performance bottlenecks before they impact user experience in production.
Master useMemo: Performance Optimization Strategy
Use useMemo when you have identified expensive calculations through profiling, not as a premature optimization. Focus on computations that filter or transform large datasets, complex mathematical operations, or create objects used as dependencies in other hooks. Remember that memoization trades memory for CPU time - profile first, optimize second, and measure the impact to ensure your optimizations actually improve performance.