Intermediate70 min read

React.memo and Performance Optimization

Learn how to optimize React component performance using React.memo, useMemo, useCallback, and other performance optimization techniques.

Topics Covered:

React.memouseMemouseCallbackPerformance OptimizationRe-render PreventionProfiling

Prerequisites:

  • Managing State with useState
  • Understanding useEffect
  • Understanding Props

Video Tutorial

Overview

Performance optimization is crucial for building fast React applications. React.memo, useMemo, and useCallback are powerful tools for preventing unnecessary re-renders and expensive recalculations. This tutorial covers when and how to use these optimization techniques, how to measure performance, and best practices for optimizing React applications.

Understanding React Re-renders

Understanding when and why components re-render is the first step to optimization. When Components Re-render: • State changes (useState, useReducer) • Props change • Parent component re-renders • Context value changes • Force update (rarely used) Why Re-renders Matter: • Can cause performance issues • Unnecessary re-renders waste resources • Can cause UI flickering • Impact user experience When Re-renders are Expensive: • Large component trees • Complex calculations • Heavy DOM manipulations • Many child components Optimization Goal: • Prevent unnecessary re-renders • Memoize expensive calculations • Optimize only when needed • Measure before optimizing

Code Example:
// Component re-renders on every parent render
function ExpensiveComponent({ data }) {
  console.log('Rendering ExpensiveComponent');
  
  // Expensive calculation runs on every render
  const result = expensiveCalculation(data);
  
  return <div>{result}</div>;
}

function Parent() {
  const [count, setCount] = useState(0);
  const [otherState, setOtherState] = useState(0);
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <button onClick={() => setOtherState(s => s + 1)}>Other: {otherState}</button>
      {/* ExpensiveComponent re-renders even when data doesn't change */}
      <ExpensiveComponent data={{ value: 42 }} />
    </div>
  );
}

// Tracking re-renders
function ComponentWithRenderTracking() {
  const renderCount = useRef(0);
  renderCount.current += 1;
  
  console.log(`Rendered ${renderCount.current} times`);
  
  return <div>Render count: {renderCount.current}</div>;
}

// When props change
function Child({ value }) {
  console.log('Child rendered with value:', value);
  return <div>{value}</div>;
}

function Parent() {
  const [value, setValue] = useState(0);
  const [other, setOther] = useState(0);
  
  return (
    <div>
      <button onClick={() => setValue(v => v + 1)}>Value: {value}</button>
      <button onClick={() => setOther(o => o + 1)}>Other: {other}</button>
      {/* Child re-renders when value changes, but also when other changes */}
      <Child value={value} />
    </div>
  );
}

Components re-render when state or props change, or when parent re-renders. Understanding re-render triggers is essential for optimization. Track renders to identify unnecessary re-renders.

Using React.memo

React.memo is a higher-order component that memoizes the result of a component. It only re-renders if props have changed. What React.memo Does: • Memoizes component render result • Compares props (shallow comparison by default) • Skips re-render if props unchanged • Only works for functional components When to Use: • Component receives same props frequently • Component is expensive to render • Parent re-renders often • Props are primitive or stable references When NOT to Use: • Component always receives new props • Props change frequently • Optimization overhead > benefit • Premature optimization

Code Example:
// Component without memo (re-renders on every parent render)
function ExpensiveChild({ name, age }) {
  console.log('Rendering ExpensiveChild');
  return (
    <div>
      <p>Name: {name}</p>
      <p>Age: {age}</p>
    </div>
  );
}

// Component with memo (only re-renders when props change)
const MemoizedChild = React.memo(function ExpensiveChild({ name, age }) {
  console.log('Rendering MemoizedChild');
  return (
    <div>
      <p>Name: {name}</p>
      <p>Age: {age}</p>
    </div>
  );
});

function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Alice');
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      {/* MemoizedChild only re-renders when name changes, not when count changes */}
      <MemoizedChild name={name} age={25} />
    </div>
  );
}

// Custom comparison function
const CustomMemoized = React.memo(
  function Component({ user, settings }) {
    return (
      <div>
        <p>{user.name}</p>
        <p>{settings.theme}</p>
      </div>
    );
  },
  (prevProps, nextProps) => {
    // Return true if props are equal (skip re-render)
    // Return false if props are different (re-render)
    return (
      prevProps.user.id === nextProps.user.id &&
      prevProps.settings.theme === nextProps.settings.theme
    );
  }
);

// Memo with object props (be careful!)
function Parent() {
  const [count, setCount] = useState(0);
  
  // ❌ BAD: New object created every render
  return <MemoizedChild data={{ value: 42 }} />;
  
  // ✅ GOOD: Stable reference
  const data = useMemo(() => ({ value: 42 }), []);
  return <MemoizedChild data={data} />;
}

React.memo prevents re-renders when props haven't changed. Use it for expensive components that receive stable props. Be careful with object/array props - they need stable references.

Using useMemo for Expensive Calculations

useMemo memoizes the result of expensive calculations, only recalculating when dependencies change. What useMemo Does: • Memoizes calculation result • Only recalculates when dependencies change • Returns cached value otherwise • Helps prevent expensive recalculations When to Use: • Expensive calculations • Derived state from props/state • Creating objects/arrays for props • Filtering/sorting large arrays When NOT to Use: • Simple calculations • Values that change frequently • Premature optimization • When overhead > benefit

Code Example:
// Expensive calculation without memo
function ProductList({ products, filter }) {
  // ❌ BAD: Recalculates on every render
  const filteredProducts = products.filter(p => 
    p.category === filter
  ).sort((a, b) => a.price - b.price);
  
  return (
    <ul>
      {filteredProducts.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

// With useMemo
function ProductList({ products, filter }) {
  // ✅ GOOD: Only recalculates when products or filter change
  const filteredProducts = useMemo(() => {
    return products
      .filter(p => p.category === filter)
      .sort((a, b) => a.price - b.price);
  }, [products, filter]);
  
  return (
    <ul>
      {filteredProducts.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

// Creating stable object references
function Component({ userId }) {
  const [count, setCount] = useState(0);
  
  // ❌ BAD: New object every render
  const config = { userId, theme: 'dark' };
  
  // ✅ GOOD: Stable reference
  const config = useMemo(
    () => ({ userId, theme: 'dark' }),
    [userId] // Only recreate when userId changes
  );
  
  return <Child config={config} />;
}

// Expensive computation
function ExpensiveCalculation({ n }) {
  const [otherState, setOtherState] = useState(0);
  
  // Expensive calculation only runs when n changes
  const result = useMemo(() => {
    console.log('Calculating...');
    let sum = 0;
    for (let i = 0; i < n * 1000000; i++) {
      sum += i;
    }
    return sum;
  }, [n]); // Only recalculate when n changes
  
  return (
    <div>
      <p>Result: {result}</p>
      <button onClick={() => setOtherState(s => s + 1)}>
        Other: {otherState}
      </button>
    </div>
  );
}

useMemo memoizes expensive calculations. Only recalculates when dependencies change. Use it for expensive operations and creating stable object/array references for props.

Using useCallback for Stable Function References

useCallback memoizes functions, returning the same function reference when dependencies haven't changed. This is crucial when passing functions as props to memoized components. What useCallback Does: • Memoizes function • Returns same reference if dependencies unchanged • Prevents unnecessary re-renders of child components • Works with React.memo When to Use: • Function passed to memoized component • Function in dependency array • Function passed to child via props • Expensive function creation When NOT to Use: • Function not passed as prop • Dependencies change frequently • Simple function creation • Premature optimization

Code Example:
// Function without useCallback
function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Alice');
  
  // ❌ BAD: New function every render
  const handleClick = () => {
    console.log('Clicked');
  };
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      {/* MemoizedChild re-renders because handleClick is new every time */}
      <MemoizedChild name={name} onClick={handleClick} />
    </div>
  );
}

// With useCallback
function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Alice');
  
  // ✅ GOOD: Stable function reference
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []); // Empty deps = function never changes
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      {/* MemoizedChild doesn't re-render when count changes */}
      <MemoizedChild name={name} onClick={handleClick} />
    </div>
  );
}

// useCallback with dependencies
function Parent({ userId }) {
  const [count, setCount] = useState(0);
  
  // Function depends on userId
  const handleUserAction = useCallback((action) => {
    console.log(`User ${userId} performed ${action}`);
    // Do something with userId
  }, [userId]); // Recreate when userId changes
  
  return <MemoizedChild onAction={handleUserAction} />;
}

// useCallback in custom hooks
function useApiCall(url) {
  const [data, setData] = useState(null);
  
  const fetchData = useCallback(async () => {
    const response = await fetch(url);
    const data = await response.json();
    setData(data);
  }, [url]); // Recreate when url changes
  
  return { data, fetchData };
}

// useCallback with event handlers
function Form() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  
  const handleSubmit = useCallback((e) => {
    e.preventDefault();
    // Submit form
    console.log({ email, password });
  }, [email, password]); // Recreate when email/password change
  
  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input value={password} onChange={(e) => setPassword(e.target.value)} />
      <button type="submit">Submit</button>
    </form>
  );
}

useCallback memoizes functions to provide stable references. Essential when passing functions to memoized components. Include all dependencies in the dependency array.

Combining Optimization Techniques

Often you need to combine React.memo, useMemo, and useCallback for optimal performance. Understanding how they work together is crucial. Combination Patterns: • React.memo + useCallback for props • React.memo + useMemo for object props • useMemo + useCallback together • Multiple optimizations in one component Best Practices: • Optimize only when needed • Measure performance first • Don't over-optimize • Use React DevTools Profiler • Test with realistic data

Code Example:
// Optimized component with all techniques
const OptimizedChild = React.memo(function Child({ 
  user, 
  settings, 
  onAction 
}) {
  // Expensive calculation memoized
  const processedData = useMemo(() => {
    return expensiveProcessing(user.data);
  }, [user.data]);
  
  return (
    <div>
      <p>{user.name}</p>
      <p>{settings.theme}</p>
      <p>{processedData}</p>
      <button onClick={onAction}>Action</button>
    </div>
  );
});

function Parent() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({ id: 1, name: 'Alice', data: [1, 2, 3] });
  const [theme, setTheme] = useState('dark');
  
  // Stable object references
  const settings = useMemo(
    () => ({ theme, language: 'en' }),
    [theme]
  );
  
  // Stable function reference
  const handleAction = useCallback(() => {
    console.log('Action performed');
  }, []);
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      {/* OptimizedChild only re-renders when user or settings actually change */}
      <OptimizedChild 
        user={user} 
        settings={settings} 
        onAction={handleAction} 
      />
    </div>
  );
}

// Complex optimization example
const ExpensiveListItem = React.memo(function ListItem({ 
  item, 
  onSelect, 
  isSelected 
}) {
  const formattedData = useMemo(() => {
    return formatItemData(item);
  }, [item]);
  
  const handleClick = useCallback(() => {
    onSelect(item.id);
  }, [item.id, onSelect]);
  
  return (
    <div 
      className={`item ${isSelected ? 'selected' : ''}`}
      onClick={handleClick}
    >
      {formattedData}
    </div>
  );
});

function List({ items, selectedId, onSelect }) {
  const handleSelect = useCallback((id) => {
    onSelect(id);
  }, [onSelect]);
  
  return (
    <div>
      {items.map(item => (
        <ExpensiveListItem
          key={item.id}
          item={item}
          isSelected={item.id === selectedId}
          onSelect={handleSelect}
        />
      ))}
    </div>
  );
}

Combine React.memo, useMemo, and useCallback for optimal performance. Memoize components, calculations, and functions. Use stable references for object/function props passed to memoized components.

Performance Profiling and Best Practices

Measuring performance is essential before optimizing. React DevTools Profiler helps identify performance bottlenecks. Profiling Tools: • React DevTools Profiler • Chrome DevTools Performance tab • Lighthouse • Web Vitals What to Measure: • Component render times • Re-render frequency • Time to interactive • Bundle size • Memory usage Best Practices: • Measure before optimizing • Optimize only when needed • Use React.memo sparingly • Don't over-optimize • Test with realistic data • Monitor in production Common Mistakes: • Optimizing too early • Memoizing everything • Forgetting dependencies • Creating new objects in render • Not measuring impact

Code Example:
// Using React DevTools Profiler
// 1. Install React DevTools browser extension
// 2. Open DevTools > Profiler tab
// 3. Click record, interact with app, stop recording
// 4. Analyze which components re-render and why

// Performance measurement hook
function useRenderTime(componentName) {
  const renderStart = useRef(performance.now());
  
  useEffect(() => {
    const renderTime = performance.now() - renderStart.current;
    console.log(`${componentName} rendered in ${renderTime.toFixed(2)}ms`);
  });
  
  useEffect(() => {
    renderStart.current = performance.now();
  });
}

// Usage
function ExpensiveComponent() {
  useRenderTime('ExpensiveComponent');
  // Component logic
}

// Performance monitoring
function usePerformanceMonitor() {
  useEffect(() => {
    // Monitor Web Vitals
    if ('PerformanceObserver' in window) {
      const observer = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          console.log('Performance entry:', entry);
        }
      });
      
      observer.observe({ entryTypes: ['measure', 'navigation'] });
      
      return () => observer.disconnect();
    }
  }, []);
}

// ✅ GOOD: Measure first, then optimize
function Component() {
  // 1. Measure performance
  // 2. Identify bottlenecks
  // 3. Apply optimizations
  // 4. Measure again to verify improvement
}

// ❌ BAD: Optimize without measuring
function OverOptimizedComponent() {
  // Memoizing everything without knowing if it helps
  const value = useMemo(() => simpleCalculation(), []);
  const handler = useCallback(() => {}, []);
  // ...
}

// ✅ GOOD: Optimize only expensive operations
function SmartComponent({ data }) {
  // Only memoize if calculation is actually expensive
  const result = data.length > 1000 
    ? useMemo(() => expensiveCalculation(data), [data])
    : simpleCalculation(data);
  
  return <div>{result}</div>;
}

// Performance best practices checklist
const OPTIMIZATION_CHECKLIST = {
  // Before optimizing:
  measure: 'Use React DevTools Profiler',
  identify: 'Find actual bottlenecks',
  test: 'Test with realistic data',
  
  // When optimizing:
  memoize: 'Use React.memo for expensive components',
  calculations: 'Use useMemo for expensive calculations',
  functions: 'Use useCallback for function props',
  stable: 'Provide stable object/array references',
  
  // After optimizing:
  verify: 'Measure again to confirm improvement',
  monitor: 'Monitor in production',
  document: 'Document why optimization was needed'
};

Measure performance before optimizing. Use React DevTools Profiler to identify bottlenecks. Optimize only when needed and measure again to verify improvements. Don't over-optimize.

Conclusion

Performance optimization is important but should be done carefully. Use React.memo for expensive components with stable props, useMemo for expensive calculations, and useCallback for stable function references. Always measure performance before optimizing, use React DevTools Profiler, and verify that optimizations actually help. Remember: premature optimization is the root of all evil - optimize only when you have evidence of a performance problem.