Performance Optimization in React

As React applications grow larger, performance can become a concern. By default, every time a component re-renders, all of its child components also re-render and all calculations inside the function run again — even when nothing related to them has changed.

React provides two Hooks specifically for performance optimization: useMemo and useCallback. These Hooks tell React to remember a calculated result or function between renders, recomputing them only when specific dependencies change.

Understanding Re-renders

React re-renders a component whenever its state or props change. Consider a component that does an expensive calculation. If an unrelated state value changes and causes a re-render, that expensive calculation runs again — unnecessarily. This is the problem that useMemo and useCallback solve.

useMemo — Memoizing a Computed Value

useMemo caches the result of a calculation and only recomputes it when the listed dependencies change. The word "memo" comes from "memoization" — a technique to store computed results for reuse.


import { useState, useMemo } from 'react';

function ExpensiveCalculation({ numbers }) {
  const [filter, setFilter] = useState("");

  // This heavy calculation only runs when 'numbers' changes
  const total = useMemo(() => {
    console.log("Calculating total...");
    return numbers.reduce((sum, n) => sum + n, 0);
  }, [numbers]);

  return (
    <div>
      <p>Total: {total}</p>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Search..."
      />
    </div>
  );
}

Without useMemo, the total would be recalculated every time the filter input changes — even though the filter has nothing to do with the calculation. With useMemo, the total only recalculates when numbers changes.

useMemo Syntax


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

The first argument is a function that returns the value to be memoized. The second argument is the dependency array — the memoized value is recomputed only when one of these dependencies changes.

Practical useMemo Example — Filtered List


import { useState, useMemo } from 'react';

const allItems = ["Apple", "Banana", "Avocado", "Blueberry", "Apricot", "Blackberry"];

function FilteredList() {
  const [query, setQuery] = useState("");

  const filteredItems = useMemo(() => {
    console.log("Filtering list...");
    return allItems.filter((item) =>
      item.toLowerCase().includes(query.toLowerCase())
    );
  }, [query]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Filter fruits..."
      />
      <ul>
        {filteredItems.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

The filtering logic only runs when query changes. If the component re-renders for any other reason, the previously computed filtered list is returned from cache.

useCallback — Memoizing a Function

While useMemo caches a value, useCallback caches a function. In JavaScript, every time a component re-renders, any function defined inside it is recreated as a new object. This matters when that function is passed as a prop to a child component — the child sees a "new" function and re-renders unnecessarily.


import { useState, useCallback } from 'react';

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  // Without useCallback, this function is recreated on every render
  const handleIncrement = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []); // Empty array — function never needs to be recreated

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <CounterButton onClick={handleIncrement} />
      <p>Count: {count}</p>
    </div>
  );
}

function CounterButton({ onClick }) {
  console.log("CounterButton rendered");
  return <button onClick={onClick}>Increment</button>;
}

Without useCallback, typing in the input causes the parent to re-render, which recreates handleIncrement, which causes CounterButton to re-render even though nothing about the button changed. With useCallback, the same function reference is reused and CounterButton does not re-render unnecessarily.

useCallback Syntax


const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

React.memo — Preventing Child Re-renders

useCallback works hand-in-hand with React.memo. React.memo wraps a component to prevent it from re-rendering unless its props actually change:


import { memo } from 'react';

const CounterButton = memo(function CounterButton({ onClick }) {
  console.log("CounterButton rendered");
  return <button onClick={onClick}>Increment</button>;
});

With React.memo, the button only re-renders if its props change. Combined with useCallback to prevent the onClick function from being recreated, unnecessary re-renders are completely eliminated.

When NOT to Use These Hooks

These Hooks add overhead themselves — they need memory to store cached values and must compare dependencies on every render. Using them everywhere can actually hurt performance instead of helping it.

Apply useMemo and useCallback only when:

  • A calculation is genuinely expensive (large arrays, complex computations)
  • A function is passed as a prop to a component wrapped in React.memo
  • A function is used as a dependency in another Hook like useEffect

For simple components and quick calculations, these Hooks are unnecessary. Profile and measure first before optimizing.

Key Points

  • useMemo caches the result of a computation and only recalculates it when dependencies change.
  • useCallback caches a function reference and only recreates it when dependencies change.
  • React.memo prevents a component from re-rendering unless its props change.
  • These optimizations are useful for expensive operations and components that receive function props.
  • Do not over-optimize — only apply these Hooks when a real performance issue has been identified.

The next topic covers Code Splitting and Lazy Loading — techniques for loading only the code that is needed at a given moment to reduce initial page load times.

Leave a Comment

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