Advanced25 min read

Performance: memo, useMemo, useCallback

Learn React's performance optimization tools: React.memo for preventing unnecessary component re-renders, useMemo for caching expensive computations, and useCallback for stable function references.

When Does React Re-render?

Before learning optimization tools, you need to understand when and why React re-renders components.

A component re-renders when:

  1. Its state changes — calling setState or dispatch triggers a re-render.
  2. Its parent re-renders — when a parent component re-renders, all of its children re-render too, even if their props have not changed.
  3. Its context value changes — when a context Provider's value changes, every consumer re-renders.

The second rule is the one that surprises most developers. Consider this example:

jsx
function Parent() {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <ExpensiveChild name="Alice" />  {/* re-renders on every count change! */}
    </div>
  );
}

function ExpensiveChild({ name }) {
  // Imagine this does heavy computation or renders a long list
  console.log('ExpensiveChild rendered');
  return <p>Hello, {name}!</p>;
}

Every time the parent's count changes, ExpensiveChild re-renders even though its name prop ("Alice") never changes. For simple components, this is fine — React's diffing is fast. But for components that are genuinely expensive (long lists, complex calculations, heavy DOM trees), these unnecessary re-renders can cause noticeable slowdowns.

React provides three tools to optimize this:

  • React.memo — skip re-rendering a component when its props have not changed
  • useMemo — cache the result of an expensive computation
  • useCallback — cache a function reference so it does not change on every render

React.memo: Memoizing Components

React.memo is a higher-order component that wraps your component and skips re-rendering if its props have not changed (using shallow comparison).

jsx
const ExpensiveChild = React.memo(function ExpensiveChild({ name }) {
  console.log('ExpensiveChild rendered');
  return <p>Hello, {name}!</p>;
});

Now ExpensiveChild will only re-render when name actually changes. When the parent re-renders due to a count update, React compares the old props ({ name: 'Alice' }) with the new props ({ name: 'Alice' }). Since they are the same (shallow equal), React skips rendering ExpensiveChild entirely.

Shallow comparison

React.memo uses shallow comparison by default. This means:

  • Primitives (strings, numbers, booleans) are compared by value: 'Alice' === 'Alice' is true.
  • Objects, arrays, and functions are compared by reference: {} === {} is false because they are different objects in memory.

This is important! If you pass an object or function as a prop, a new reference is created on every render:

jsx
function Parent() {
  const [count, setCount] = React.useState(0);

  // NEW object every render — defeats React.memo!
  const style = { color: 'red' };

  // NEW function every render — also defeats React.memo!
  const handleClick = () => console.log('clicked');

  return <MemoChild style={style} onClick={handleClick} />;
}

Even though the values are logically the same, they are new references each time, so React.memo sees them as "changed" and re-renders anyway. This is where useMemo and useCallback come in.

Custom comparison function

You can pass a custom comparison function as the second argument to React.memo:

jsx
const MemoChild = React.memo(MyComponent, (prevProps, nextProps) => {
  return prevProps.id === nextProps.id; // only re-render if id changes
});

Return true to skip re-rendering, false to allow it.

useMemo: Caching Expensive Computations

useMemo takes a function and a dependency array, and returns the memoized result. React only re-runs the function when one of the dependencies changes.

jsx
const memoizedValue = React.useMemo(() => {
  return computeExpensiveValue(a, b);
}, [a, b]);

When to use useMemo

1. Expensive calculations:

jsx
function FilteredList({ items, filter }) {
  // Without useMemo: runs on EVERY render
  // With useMemo: runs only when items or filter change
  const filtered = React.useMemo(() => {
    console.log('Filtering...');
    return items.filter(item =>
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);

  return (
    <ul>
      {filtered.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

2. Stabilizing object/array references for React.memo children:

jsx
function Parent() {
  const [count, setCount] = React.useState(0);

  // Without useMemo: new array every render
  // With useMemo: same reference unless data changes
  const items = React.useMemo(() => ['Apple', 'Banana', 'Cherry'], []);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <MemoizedList items={items} />
    </div>
  );
}

Because items has a stable reference (same array object), the memoized child will correctly skip re-rendering when count changes.

How useMemo works

On the first render, React calls the function and stores both the result and the dependencies. On subsequent renders, React compares the current dependencies with the stored ones. If they are the same (shallow comparison), React returns the stored result without calling the function again. If they differ, React calls the function and stores the new result.

useCallback: Stable Function References

useCallback is essentially useMemo for functions. It returns a memoized version of the callback that only changes when its dependencies change.

jsx
const memoizedFn = React.useCallback(() => {
  doSomething(a, b);
}, [a, b]);

This is equivalent to:

jsx
const memoizedFn = React.useMemo(() => {
  return () => doSomething(a, b);
}, [a, b]);

Why useCallback matters

In JavaScript, every function expression creates a new function object:

jsx
function Parent() {
  const [count, setCount] = React.useState(0);

  // Without useCallback: new function every render
  const handleClick = () => console.log('clicked');

  // With useCallback: same function reference across renders
  const handleClickStable = React.useCallback(() => {
    console.log('clicked');
  }, []);

  return <MemoChild onClick={handleClickStable} />;
}

Without useCallback, handleClick is a new function on every render, which means React.memo on MemoChild would be useless — it would always see a "new" onClick prop.

With useCallback, handleClickStable is the same function reference across renders (as long as its dependencies do not change), so React.memo can correctly skip re-rendering.

When to use useCallback

Use useCallback when:

  • You pass a function to a memoized child component (React.memo)
  • You pass a function as a dependency of useEffect, useMemo, or another hook
  • The function is used as a stable reference in event subscriptions

Do NOT use useCallback everywhere. It adds complexity and memory overhead. Only use it when there is a measurable benefit.

When NOT to Optimize

The most important performance optimization concept is knowing when NOT to optimize. Premature optimization is a common trap in React development.

Do not optimize by default

React's rendering is very fast. For most components, the cost of re-rendering is negligible (microseconds). Adding React.memo, useMemo, and useCallback everywhere has its own costs:

  • Memory overhead — memoized values are stored in memory
  • Comparison cost — React must compare props/dependencies on every render
  • Code complexity — more hooks means harder-to-read code
  • Stale values — incorrect dependency arrays can cause bugs that are hard to track down

The optimization checklist

Before reaching for optimization tools, ask yourself:

  1. Is there actually a performance problem? If the app feels fast, do not optimize.
  2. Can you measure the improvement? Use React DevTools Profiler to see how long renders take.
  3. Is the component genuinely expensive? A component that renders 3 <p> tags does not need React.memo.
  4. Have you tried simpler fixes first? Often, moving state closer to where it is needed (lifting or colocating state) eliminates unnecessary re-renders without any memoization.

Profiling basics

React DevTools includes a Profiler tab that records renders:

  1. Open React DevTools and click the Profiler tab
  2. Click "Record" and interact with your app
  3. Click "Stop" to see a flame graph of renders
  4. Each bar shows a component, its render time, and whether it re-rendered

Look for components that:

  • Re-render frequently (many bars)
  • Take a long time per render (wide bars)
  • Re-render when they should not (gray bars become green when you add React.memo)

Profile first, then optimize the bottlenecks. Never guess where the performance problem is.

Performance Optimization in Action

html
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
  // Memoized child — only re-renders when items change
  const ExpensiveList = React.memo(function ExpensiveList({ items }) {
    console.log("ExpensiveList rendered");
    return (
      <ul id="list">
        {items.map((item, i) => <li key={i}>{item}</li>)}
      </ul>
    );
  });

  function App() {
    const [count, setCount] = React.useState(0);

    // useMemo: stable array reference
    const items = React.useMemo(() => ["React", "Vue", "Angular"], []);

    // useCallback: stable function reference
    const increment = React.useCallback(() => {
      setCount(c => c + 1);
    }, []);

    return (
      <div>
        <h2>Performance Demo</h2>
        <p>Count: {count}</p>
        <button onClick={increment}>Increment</button>
        <p id="render-info">Items: {items.length}</p>
        <ExpensiveList items={items} />
        <p><em>Open console — ExpensiveList does not re-render on count change!</em></p>
      </div>
    );
  }

  ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>

What is the main risk of using React.memo, useMemo, and useCallback everywhere?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.