# doing less in React

2021-06-19 · 4 min read · #react · #hooks · #performance

Source: https://jopcmelo.com/writing/doing-less-in-react

once you know [why a component renders](/writing/why-did-this-render), making React faster mostly means making it do less: fewer renders, less work in each one, less code to download, fewer DOM nodes.

## skip renders: memo

a component renders when its parent does. [`memo`](https://react.dev/reference/react/memo) adds a check: if every prop is the same as last time, skip it. "the same" means `Object.is` on each prop, a shallow check. `{} === {}` is false in JavaScript, so an object or a function created during the parent's render is never the same as last time.

that's the trap. I rendered a list of 1,000 items, changed the list's state, and counted how many items rendered again (React 19, in jsdom):

- no `memo`: 1,000.
- `memo`, passing `onSelect={(id) => setSelected(id)}` to each item: still 1,000, because that arrow function is new on every render.
- `memo`, with the function wrapped in `useCallback`: 0.

*(sketch: a List and four Items, twice. on top, without memo, every box flashes each time the list's state changes. below, with memo and useCallback, only the List flashes)*

```jsx
const Item = memo(function Item({ item, onSelect }) {
  return <li onClick={() => onSelect(item.id)}>{item.title}</li>;
});

function List({ items }) {
  const [selected, setSelected] = useState(null);
  const select = useCallback((id) => setSelected(id), []);

  return (
    <ul>
      {items.map((item) => (
        <Item key={item.id} item={item} onSelect={select} />
      ))}
    </ul>
  );
}
```

**memo on its own did nothing here.** it only works when every prop keeps its identity between renders, so it comes with `useCallback` for functions and `useMemo` for objects and arrays.

## skip work: useMemo

when a component computes something on every render, [`useMemo`](https://react.dev/reference/react/useMemo) keeps the result until its inputs change:

```jsx
function Cart({ items }) {
  const total = useMemo(
    () => items.reduce((sum, item) => sum + item.price, 0),
    [items],
  );

  return <p>total: {total}</p>;
}
```

the array at the end is what makes it work. leave it out and the function runs on every render, same as without `useMemo`.

summing a cart isn't expensive, though. **most values are cheaper to compute again than to remember**, and React's docs suggest timing it first: memoizing starts to make sense [around 1ms or more](https://react.dev/reference/react/useMemo#how-to-tell-if-a-calculation-is-expensive).

## or let the compiler do it

since [React Compiler 1.0](https://react.dev/blog/2025/10/07/react-compiler-1), in October 2025, a build step can do this memoization for you. with it on, the inline arrow function above stops breaking `memo`, and you write far fewer `useMemo` and `useCallback` calls by hand.

## skip downloads: lazy

everything you import ends up in the bundle, including a settings panel most people never open. [`lazy`](https://react.dev/reference/react/lazy) puts a component in its own file, downloaded the first time it renders:

```jsx
const Settings = lazy(() => import('./settings'));

function App() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button onClick={() => setOpen(true)}>settings</button>
      {open && (
        <Suspense fallback={<p>loading</p>}>
          <Settings />
        </Suspense>
      )}
    </>
  );
}
```

*(sketch: the main bundle holds App, Header, List and Item. Settings sits apart in its own file, fetched on first open)*

in Next.js, [`next/dynamic`](https://nextjs.org/docs/app/guides/lazy-loading) does the same thing, with a `loading` option where React uses `Suspense`. and to see what's in the bundle in the first place, [`@next/bundle-analyzer`](https://www.npmjs.com/package/@next/bundle-analyzer) draws it as a map, with the biggest files in the biggest boxes.

## skip DOM nodes: virtual lists

a list of 10,000 rows puts 10,000 rows in the DOM, even though a screen only fits a few dozen. a virtual list renders the rows in view, plus a few above and below, and swaps them as you scroll. [react-window](https://github.com/bvaughn/react-window) does this. it comes from the author of react-virtualized, whose README points people to it as the lighter option.

*(sketch: a long list of rows with a frame around the few on screen. those are drawn solid, in the DOM. the rows above and below are faint, not in the DOM)*

**the fastest row to render is the one that isn't there.**
