Skip to content
Topic 11 of 13
Theory

Performance: memo, useMemo, useCallback

First — don't fear re-renders

You already know that a component re-renders when its state changes and when its parent renders. Beginners often panic: "everything re-renders at the slightest sneeze!". But rendering is a function call and an object comparison, usually a fraction of a millisecond. The real DOM changes only where there's a difference. In most applications nothing needs to be optimized.

Optimization is needed when there's a noticeable problem: an input field "lags", a list of thousands of rows re-renders entirely, a heavy calculation runs somewhere. Then React has three tools: useMemo, memo and useCallback. You definitely need to understand them — they're in any real project — and use them wisely.

useMemo: remember the result of a calculation

const { useMemo } = React;

function ProductList({ products, query }) {
  const [dark, setDark] = useState(false);

  const visible = useMemo(
    () => products.filter((p) => p.title.toLowerCase().includes(query.toLowerCase())),
    [products, query],
  );

  return (
    <div className={dark ? "dark" : ""}>
      <button onClick={() => setDark(!dark)}>Theme</button>
      <ul>{visible.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
    </div>
  );
}
  • useMemo(function, [dependencies]) calls the function on the first render and remembers the result.
  • On the following renders, if the dependencies haven't changed, it returns the remembered value right away without calling the function.
  • Switching the theme no longer filters the list again: products and query are the same.

For a filter over a hundred products you don't need it — it's fast anyway. useMemo is needed when the calculation is really heavy (thousands of elements, complex math) or when it's important to keep the same reference to an object (more on that below).

memo: skip a component's render

memo wraps a component and tells React: "if the props are the same as last time, don't call me — use the previous result":

const { memo } = React;

const UserCard = memo(function UserCard({ name, avatar }) {
  console.log("render", name);
  return (
    <div className="card">
      <img src={avatar} alt="" />
      <p>{name}</p>
    </div>
  );
});

Now when the parent renders, UserCard re-renders only if name or avatar changed. Its own state and context changes re-render the component as usual — memo only looks at props.

The trap: comparing by reference

memo compares each prop with Object.is — for objects, arrays and functions that's a comparison of references. And in the component body new objects and functions are created on every render:

function Parent() {
  const [n, setN] = useState(0);

  const style = { color: "red" };           // a new object every time
  const handleClick = () => console.log(1); // a new function every time

  return (
    <>
      <button onClick={() => setN(n + 1)}>{n}</button>
      <Child style={style} onClick={handleClick} />   {/* memo won't help! */}
    </>
  );
}

For memo these are "new props" every time, and Child re-renders anyway. For memo to work, the objects and functions in props must keep their reference between renders:

  • constants — move them outside the component;
  • computed objects and arrays — useMemo;
  • functions — useCallback.

useCallback: a stable function

const { useCallback } = React;

function TodoList() {
  const [todos, setTodos] = useState([]);

  const handleDelete = useCallback((id) => {
    setTodos((list) => list.filter((t) => t.id !== id));
  }, []);

  return todos.map((t) => <TodoItem key={t.id} todo={t} onDelete={handleDelete} />);
}

const TodoItem = memo(function TodoItem({ todo, onDelete }) {
  return <li onClick={() => onDelete(todo.id)}>{todo.text}</li>;
});

useCallback(function, [dependencies]) returns the very same function as long as the dependencies haven't changed. In essence it's useMemo(() => function, dependencies).

Note setTodos((list) => …). If there were todos.filter(…) inside, todos would have to be added to the dependencies — and the function would change on every change of the list. The updater function takes the fresh list from its argument, so there are no dependencies and the function is stable forever.

How to find what's slow

Don't guess — measure:

  • React Developer Tools (a browser extension) → the Profiler tab: record an action and see which components re-rendered and how long it took;
  • in its settings — "Highlight updates": re-rendered components are highlighted on the page;
  • console.log in the component body — the simplest render counter.

Common real causes of slowness: very long lists (the fix is to show only the visible part, "virtualization"), heavy calculations on every render, state too high in the tree (an input field at the root of the application re-renders the whole application — move the state down to where it's needed).

When NOT to optimize

  • useMemo and useCallback themselves cost time and memory and complicate the code. Wrapping everything in them is harmful.
  • useCallback without memo on the child component is almost useless: if the child isn't memoized, it re-renders anyway.
  • First — the right structure: state as low as possible, calculations during rendering, no extra effects. That solves most problems without a single memo.

Newer versions of React are getting the React Compiler, which adds such memoization automatically. But you still need to understand what it does and why.

Summary

  • Rendering is usually cheap. Optimize when there's a measured problem.
  • useMemo(() => calculation, [deps]) — remember the result of a heavy calculation or an object.
  • memo(Component) — skip the render when the props are the same.
  • Props are compared by reference: new objects and functions break memo.
  • useCallback(fn, [deps]) — the same function between renders; together with setX((old) => …) — without extra dependencies.
  • Measure with the React DevTools Profiler.

A heavy calculation

Easy+10 XP

The Primes component with a prop limit shows how many prime numbers there are up to limit (the countPrimes function is already written and is slow), and a Theme button that switches the theme.

Right now countPrimes is called on every render — even when only the theme changes. Make it recalculate only when limit changes.

click Theme    → countPrimes isn't called
limit 100→200  → countPrimes is called once

Connect the connector to solve tasks

Task code runs on your own computer — faster and more reliable than in the browser. This needs the free CodeWithMe connector app. You only connect it once.

Looking for the connector on this computer…

  1. 1
    Download the connector for your system (Windows, macOS or Linux).Download the connectorOn the download page pick the file with your system in its name, e.g. windows-amd64.
  2. 2Run it and choose a folder for your solutions. It will install Python and Node.js there by itself when needed.
  3. 3
    Enter below the 6-digit code shown in the connector window (“Code for CodeWithMe”). If the browser asks about local network access — allow it.

The connector is open source (on GitHub), works only on your computer and runs code only after you enter the pairing code.