ReactJs Memoization and Performance Optimization

React applications can suffer performance issues when components re-render unnecessarily. Memoization techniques help optimize performance by preventing expensive computations and component re-renders when their inputs haven’t changed. However, premature optimization can add complexity without meaningful benefits. Understanding when and how to use memoization is critical for building performant React applications.

Understanding React Re-renders

React re-renders a component when:

  1. Its state changes
  2. Its props change
  3. Its parent re-renders
  4. A context it consumes changes

By default, when a parent component re-renders, all its children re-render too, even if their props haven’t changed. This is usually fine, as React’s reconciliation is fast. However, for expensive components or deeply nested trees, unnecessary re-renders can cause performance issues.

React.memo

React.memo is a higher-order component that memoizes the rendered output of a function component. It only re-renders the component if its props have changed:

import { memo } from 'react';

const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
  // Expensive computation or rendering
  const processedData = expensiveOperation(data);
  
  return <div>{processedData}</div>;
});

With React.memo, the component will skip rendering if the props are shallowly equal to the previous props.

Custom Comparison Function

By default, React.memo uses shallow comparison for props. For custom comparison logic, provide a second argument:

const MyComponent = memo(function MyComponent({ user }) {
  return <div>{user.name}</div>;
}, (prevProps, nextProps) => {
  // Return true if passing nextProps would render the same result as prevProps
  return prevProps.user.id === nextProps.user.id;
});

When to Use React.memo

Use React.memo when:

  • The component renders often with the same props
  • The component’s render output is expensive to compute
  • The component is a pure function of its props
  • Profiling shows the component causes performance issues

Don’t use React.memo when:

  • The component rarely re-renders with the same props
  • The render is cheap
  • Props change on almost every render
  • You haven’t measured a performance problem

useMemo Hook

The useMemo Hook memoizes the result of an expensive computation:

import { useMemo } from 'react';

function ProductList({ products, filter }) {
  const filteredProducts = useMemo(() => {
    // Expensive filtering operation
    return products.filter(product => {
      return product.category === filter && 
             product.inStock && 
             expensiveCheck(product);
    });
  }, [products, filter]);
  
  return (
    <ul>
      {filteredProducts.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

Without useMemo, the filtering operation runs on every render, even if products and filter haven’t changed.

useMemo vs Calculating on Every Render

// Without useMemo - recalculates every render
function Component({ items }) {
  const total = items.reduce((sum, item) => sum + item.price, 0);
  return <div>Total: {total}</div>;
}

// With useMemo - only recalculates when items change
function Component({ items }) {
  const total = useMemo(
    () => items.reduce((sum, item) => sum + item.price, 0),
    [items]
  );
  return <div>Total: {total}</div>;
}

Use useMemo only when the computation is genuinely expensive. For simple calculations, the overhead of useMemo can be worse than just recalculating.

useCallback Hook

The useCallback Hook memoizes a callback function, preventing it from being recreated on every render:

import { useCallback } from 'react';

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [other, setOther] = useState(0);
  
  // Without useCallback, this creates a new function on every render
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []);
  
  return (
    <div>
      <ExpensiveChild onClick={handleClick} />
      <button onClick={() => setOther(other + 1)}>
        Other: {other}
      </button>
    </div>
  );
}

const ExpensiveChild = memo(function ExpensiveChild({ onClick }) {
  // Without useCallback, this would re-render every time ParentComponent renders
  return <button onClick={onClick}>Click me</button>;
});

useCallback is most useful when:

  • Passing callbacks to memoized child components
  • Using the callback as a dependency in other hooks
  • The callback is passed to many components

Before You memo()

Dan Abramov’s article Before You memo() provides excellent guidance on optimizing React components. Key takeaways:

Move State Down

If only part of a component needs state, move that state to a child component:

// Before - entire component re-renders when color changes
function App() {
  const [color, setColor] = useState('blue');
  
  return (
    <div>
      <ExpensiveTree />
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <div style=>Colored text</div>
    </div>
  );
}

// After - only ColorPicker re-renders when color changes
function App() {
  return (
    <div>
      <ExpensiveTree />
      <ColorPicker />
    </div>
  );
}

function ColorPicker() {
  const [color, setColor] = useState('blue');
  
  return (
    <>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <div style=>Colored text</div>
    </>
  );
}

Lift Content Up

If state affects a wrapper but not its children, lift the children up as props:

// Before - children re-render when state changes
function App() {
  const [state, setState] = useState(0);
  
  return (
    <div onClick={() => setState(state + 1)}>
      <ExpensiveComponent />
    </div>
  );
}

// After - children don't re-render when state changes
function App() {
  return <Wrapper><ExpensiveComponent /></Wrapper>;
}

function Wrapper({ children }) {
  const [state, setState] = useState(0);
  
  return (
    <div onClick={() => setState(state + 1)}>
      {children}
    </div>
  );
}

The children prop is the same object across renders, so React knows it doesn’t need to re-render it.

Measuring Performance

Always measure before optimizing. Use React DevTools Profiler to identify performance bottlenecks:

  1. Open React DevTools
  2. Go to the Profiler tab
  3. Click the record button
  4. Interact with your application
  5. Stop recording
  6. Analyze which components render and how long they take

Look for:

  • Components that render frequently
  • Components with long render times
  • Unnecessary re-renders

Common Pitfalls

Over-Optimization

Adding memoization everywhere adds complexity and maintenance burden. Start simple, measure performance, and optimize only what matters.

Incorrect Dependencies

Forgetting dependencies in useMemo or useCallback can lead to stale closures:

// Bug - total will always use the initial discount value
const total = useMemo(() => {
  return items.reduce((sum, item) => sum + item.price * (1 - discount), 0);
}, [items]); // Missing discount dependency!

// Correct
const total = useMemo(() => {
  return items.reduce((sum, item) => sum + item.price * (1 - discount), 0);
}, [items, discount]);

Memoizing Everything

Memoization has overhead. For simple components and cheap calculations, the cost of memoization can exceed the cost of re-rendering.

Premature Optimization

The React team’s guidance is clear: optimize after you measure a problem. Modern React is fast enough for most use cases without manual optimization.

Performance Patterns

Virtualization for Long Lists

For lists with hundreds or thousands of items, use virtualization libraries like react-window or react-virtualized:

import { FixedSizeList } from 'react-window';

function LargeList({ items }) {
  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>{items[index].name}</div>
      )}
    </FixedSizeList>
  );
}

Virtualization renders only visible items, dramatically improving performance for large lists.

Code Splitting

Split large components into separate bundles loaded on demand:

import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <HeavyComponent />
    </Suspense>
  );
}

Debouncing Expensive Operations

For operations triggered by user input, debounce to reduce frequency:

import { useState, useEffect } from 'react';

function SearchResults({ query }) {
  const [debouncedQuery, setDebouncedQuery] = useState(query);
  
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedQuery(query);
    }, 300);
    
    return () => clearTimeout(timer);
  }, [query]);
  
  // Use debouncedQuery for expensive search
  const results = useMemo(
    () => expensiveSearch(debouncedQuery),
    [debouncedQuery]
  );
  
  return <div>{results}</div>;
}

Resources