# Performance Optimization — Part 1: Measuring and `memo`

Prerequisite: `frontend/react/hooks/06-usememo`, `07-usecallback` (the mechanics of each hook) and `frontend/react/core/04-rendering_and_reconciliation` (render vs commit, fiber, lanes). This part is about the applied discipline of *when* to reach for those tools, plus `React.memo`, which those two prerequisites don't cover.

## 1. Profile Before You Optimize

Every optimization in this topic has a real cost — extra memory for cached values, extra comparison work per render, more code to maintain. Applying any of them without evidence of an actual problem usually makes things slower, not faster: `memo` still has to run a comparison on every parent render, and `useMemo`/`useCallback` still allocate a dependency array to check every render.

The React DevTools **Profiler** tab is the source of truth: record an interaction, and it shows which components rendered, how long each took, and — critically — *why* each one rendered (a badge naming the changed prop, state, context, or "parent re-rendered"). Optimize the components the profiler flags as both slow and frequently re-rendering; leave everything else alone.

## 2. Why a Component Re-renders at All

Three, and only three, root triggers cause a component to render:

1. Its own state changed (`useState`/`useReducer`).
2. Its parent re-rendered (and the component isn't bailed out — see Section 3).
3. A context it subscribes to changed identity (`frontend/react/advanced/01-context_api_in_depth`).

Everything else — a prop changing, a key changing — is a *consequence* of one of these three, propagated down through the tree.

## 3. `React.memo`: Skipping Re-render on Unchanged Props

`memo` wraps a component so that, on a parent re-render, React first shallow-compares the new props object to the old one; if every prop is `Object.is`-equal, React skips calling the component function and reuses last render's output.

```tsx
const ExpensiveRow = memo(function ExpensiveRow({ label, value }: { label: string; value: number }) {
  console.log("rendering", label);
  return <tr><td>{label}</td><td>{value}</td></tr>;
});

function Table({ rows, unrelatedCounter }: { rows: Row[]; unrelatedCounter: number }) {
  // unrelatedCounter changing re-renders Table, but each ExpensiveRow
  // only re-renders if ITS OWN row object changed reference
  return <table>{rows.map((r) => <ExpensiveRow key={r.id} label={r.label} value={r.value} />)}</table>;
}
```

Without `memo`, every `Table` re-render would re-render every `ExpensiveRow`, regardless of whether that row's data changed.

## 4. The `memo` + `useCallback`/`useMemo` Pairing

`memo`'s shallow comparison is only useful if the props it's comparing are actually stable across renders where nothing relevant changed. A function or object prop built inline defeats it immediately:

```tsx
function Table({ rows }: { rows: Row[] }) {
  // BAD: a new function every render — ExpensiveRow's memo never bails out
  return <>{rows.map((r) => <ExpensiveRow key={r.id} row={r} onSelect={() => select(r.id)} />)}</>;
}

function Table({ rows }: { rows: Row[] }) {
  // GOOD: onSelect is stable across renders where `select` didn't change
  const onSelect = useCallback((id: string) => select(id), [select]);
  return <>{rows.map((r) => <ExpensiveRow key={r.id} row={r} onSelect={onSelect} />)}</>;
}
```

This is the applied version of the identity rule from `useMemo`/`useCallback`'s own theory: those hooks exist largely *to make `memo` work*. Using one without the other is a common half-fix — wrapping a child in `memo` while the parent still hands it a fresh object every render accomplishes nothing.

## 5. `memo` with a Custom Comparator

`memo`'s second argument overrides the default shallow comparison, useful when a prop is an object whose *identity* changes but whose *relevant fields* don't:

```tsx
const UserCard = memo(
  function UserCard({ user }: { user: { id: string; name: string; lastSeen: number } }) {
    return <div>{user.name}</div>;
  },
  (prevProps, nextProps) => prevProps.user.id === nextProps.user.id && prevProps.user.name === nextProps.user.name,
  // deliberately ignoring lastSeen — this card doesn't render it
);
```

Return `true` from the comparator to **skip** the re-render (props treated as equal) — this is the inverse convention from `shouldComponentUpdate`, and a common source of bugs when developers get the boolean backwards.

## 6. Under the Hood: What the Shallow Comparison Actually Does

![memo's default comparator loops keys of both prop objects with Object.is; a custom comparator replaces that loop entirely](diagrams/01-memo-comparison.svg)

`memo`'s default comparator is not a deep equal — it's `shallowEqual`, which does exactly this:

```
function shallowEqual(objA, objB) {
  if (Object.is(objA, objB)) return true;
  if (typeof objA !== "object" || objA === null) return false;
  if (typeof objB !== "object" || objB === null) return false;
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);
  if (keysA.length !== keysB.length) return false;
  for (const key of keysA) {
    if (!Object.is(objA[key], objB[key])) return false;
  }
  return true;
}
```

This runs on the fiber during `beginWork` for a memo-type fiber: React reads `oldProps` from the current (already-committed) fiber and `newProps` from the pending work, runs the comparator, and if it returns `true`, React **bails out** — it clones the child fiber subtree from the previous commit instead of calling the function again. Two things this makes precise:

- **One level deep only.** A `user` object with an unchanged `id` but a mutated `preferences` object nested inside it *passes* the default shallow comparison because `Object.is(oldUser, newUser)` already short-circuits `true` when the reference itself didn't change — but if a *new* `user` object is passed with only `preferences` different, `shallowEqual` correctly returns `false` because `user` itself is a top-level key. The pitfall is the other direction: a prop that is itself a nested object whose leaf value changed while a *wrapper* stayed structurally similar can still slip through a *custom* comparator that doesn't check deep enough.
- **`memo` cannot stop context propagation.** As covered in topic 01 Section 3, `propagateContextChange` sets `childLanes` on every ancestor of a matching consumer, and a memo fiber with non-zero `childLanes` is not allowed to bail out even if its shallow prop comparison passes — React must still descend through it. `memo` only protects against *parent re-renders via props*; it does nothing against context or the component's own state.

## 7. Common Pitfalls

1. **Wrapping every component in `memo` "for safety."** The comparison itself costs a pass over every prop, every render, whether or not it ever helps — blanket `memo` usage without profiling evidence commonly makes an app slower.
2. **`memo` around a component whose parent always passes new object/function props.** The comparison always fails, so `memo` adds a wasted comparison on top of the render it couldn't prevent — pair it with `useMemo`/`useCallback` at the prop's source, per Section 4.
3. **Forgetting `memo` does nothing against context.** A `memo`-wrapped consumer still re-renders on every provider value change (Section 6) — the fix is splitting contexts or a selector-based store, not more `memo`.
4. **Returning the comparator's boolean backwards.** `memo`'s custom comparator returns `true` to SKIP a render (props "equal") — the opposite of a typical `equals(a, b)` utility's intuitive "true means different" framing bugs people expect.
5. **Optimizing components the Profiler didn't flag.** Time spent memoizing a component that renders in under a millisecond and rarely re-renders is time not spent on the one that actually causes jank.
6. **Passing `children` as JSX and expecting `memo` to catch it.** `<Wrapper><Child /></Wrapper>` creates a new `children` element (and thus a new prop value) on every parent render unless the JSX itself is hoisted or the element reference is otherwise stabilized — this is a common "why doesn't memo work here" report.

## 8. Quick Reference

| Tool | Protects against | Does NOT protect against |
|---|---|---|
| `React.memo` | Re-render caused by unchanged props on parent re-render | Own state change, context change, non-memoized prop sources |
| `useCallback` | New function identity breaking `memo`/`useMemo`/`useEffect` deps | Anything if the memoized child isn't itself wrapped in `memo` |
| `useMemo` | Recomputing an expensive value on every render | Anything if nothing downstream depends on referential stability |
| Custom `memo` comparator | False negatives from objects with irrelevant changing fields | Bugs from the inverted `true`-means-skip boolean convention |

| Step | Tool |
|---|---|
| Find what's actually slow | React DevTools Profiler, record + inspect "why did this render" |
| Stop a child re-rendering on unrelated parent renders | `memo` |
| Make a function/object prop worth comparing | `useCallback` / `useMemo` at the source |
| Stop re-renders driven by context, not props | Split contexts or selector-based store (topic 01, Section 7) |

Continue to `react_performance_optimization_theory_2.md` for code splitting, list virtualization, and the concurrent-rendering APIs (`useTransition`, `useDeferredValue`).
