Not memoizing expensive computations using useMemo

Loading

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

  1. Wasted CPU Cycles: Recalculating on every render
  2. Performance Bottlenecks: Especially with large datasets
  3. Unnecessary Child Renders: Downstream components re-render
  4. 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

  1. Expensive Calculations: Sorting, filtering, transforming large datasets
  2. Stable References: When passing objects/arrays to optimized children
  3. Derived State: Complex state derivations
  4. Component Initialization: Heavy setup computations

When NOT to Use useMemo

  1. Simple Calculations: Basic math, small array operations
  2. Primitive Values: No need to memoize strings, numbers, booleans
  3. Every Render is Fast: If computation is cheap
  4. Dependencies Change Often: Memoization provides no benefit

Best Practices

  1. Profile First: Use React DevTools to identify bottlenecks
  2. Correct Dependencies: Include all values used in the computation
  3. Avoid Premature Optimization: Only optimize when needed
  4. Combine with useCallback: For stable function references
  5. Type Safety: With TypeScript, type the memoized value
const result = useMemo<ReturnType>(() => {...}, [deps]);

Common Pitfalls

  1. Missing Dependencies:
   const filtered = useMemo(() => items.filter(/* uses count */), [items]); 
   // ❌ Missing count dependency
  1. Over-Memoization:
   const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
   // ❌ Too simple to need memoization
  1. Impure Functions:
   const data = useMemo(() => {
     return items.map(item => ({ ...item, id: Math.random() }));
     // ❌ Different result with same dependencies
   }, [items]);
  1. 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

  1. useMemo Caches Calculations: Between re-renders
  2. Dependency Array Matters: Like useEffect
  3. Not a Silver Bullet: Only for measurable performance issues
  4. Combine with React.memo: For complete optimization
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *