skip to content

Your team's React app is slow and the proposed fix is to wrap most components in React.memo. As the lead, when do you require restructuring the component tree first, and when is a memo boundary the right tool?

level: principalimportance: should knowfreq 32%

answer

  1. cache versus deleted work
  2. memo has a precondition, structure does not
  3. find the state owner in a profile
  4. few named boundaries, not blanket coverage
  5. compiler changes cost, not diagnosis

basics

~20 s

Restructure first, because moving state down or relaying subtrees as content deletes the render work outright with no precondition to maintain. Reserve memo boundaries for the few places where the update genuinely must originate high and feed an expensive subtree unchanged props.

solid answer

~60 s

Blanket `React.memo` trades a diagnosis for a cache. Each boundary adds a shallow comparison on every parent render, retains the previous props, and carries a hidden precondition — every prop must stay referentially stable — that a future inline object or arrow function will quietly break, with no error and no failing test. Restructuring has no such contract: moving state into the component that reads it, splitting a component with unrelated concerns, or relaying an expensive subtree through `children` removes the work permanently and survives refactors. So my rule is: profile, find the state owner that is dragging expensive descendants along, and try to move the boundary. Accept a memo boundary when the update legitimately originates high and cannot move — a shared live value several regions consume, or a list feeding hundreds of rows unchanged props — and then place few, deliberate boundaries at high-fanout points rather than sprinkling them everywhere. The React Compiler shifts how much hand memoization is worth writing; it does not decide where state should live.

go deeper

for a junior

Know that React.memo is a cache with conditions, not a switch that makes components fast, and that where state lives is the first thing to look at when a page re-renders too much.

for a middle

Explain the concrete failure mode you are avoiding: one inline object or arrow prop makes a memo boundary useless while it keeps paying for the comparison, and nothing in the build tells you.

for a senior

Show the diagnosis order on a real case — profile, locate the state owner, try moving state or relaying the subtree, and only then place a boundary you can justify prop by prop.

for a principal

Argue the organisational angle: blanket memoization makes a codebase unanalysable, so the rule exists to protect diagnosability as much as frame time. Set the review bar, and require boundaries to be revisited or deleted when the structure they guarded changes.

## Two different kinds of fix A React tree can be made cheaper in two fundamentally different ways. **Structural** — change who owns state and who authors JSX, so the expensive work is never scheduled: move state into the component that actually reads it, split a component that fuses unrelated concerns into one render boundary, pass an expensive subtree as `children` so its elements keep identity, or hoist invariant JSX out of a component that re-renders often. **Memoized** — leave the tree as it is and add caches that skip work after the fact: `React.memo` around a component, `useMemo` and `useCallback` to keep props stable enough for that comparison to succeed. They are not equivalent, and the difference is durability, not speed. ## Why structure comes first - **It removes work rather than checking for it.** A memoized component is still visited: React renders the parent, builds the element, and runs a shallow prop comparison. A restructured tree does not even reach the subtree. - **It has no precondition.** `React.memo` only pays off while every prop stays referentially stable. Six months later someone adds `style={{ marginTop: 8 }}` or an inline arrow and the boundary silently becomes pure overhead. Nothing fails; the app just gets slow again. Moving state down cannot be undone by adding a prop. - **It carries no maintenance surface.** Memoization multiplies dependency arrays and `useCallback` wrappers up the tree — the boundary at the bottom forces stability obligations on every ancestor that feeds it. - **It reads better.** A small component owning a small piece of state explains itself. `memo(Component)` explains nothing about why it was needed, and nobody dares remove it later. - **It compounds.** Structure that keeps fast-changing state low keeps working as features are added; a memo boundary protects exactly the props it was written against. ## When a memo boundary is genuinely right Structure-first is a default, not an absolute. Legitimate cases: - **The update truly must originate high.** A live value several distant regions consume, where no lower owner exists and relaying content is not possible because the state owner really does author the subtree. - **A wide fan-out of unchanged children.** A parent that re-renders for its own valid reasons while rendering hundreds of rows with unchanged props — one boundary at the row component buys a lot for one comparison each. - **An expensive pure computation on stable inputs**, which is a `useMemo` question rather than a component-boundary question. - **Library and package boundaries**, where you cannot control how consumers structure their trees. Notice the shape: memo earns its place where the re-render source is legitimately immovable and the fan-out is large. That is a small set of places in most apps, which is why the policy is "few, named boundaries", not "memo by default". ## What the React Compiler changes The React Compiler (`babel-plugin-react-compiler`) applies memoization at build time, including caching the element objects a component creates, so much of the hand-written `useMemo` and `useCallback` scaffolding stops being worth writing. What it does not do is decide *where state belongs*. A component that owns fast-changing state still re-runs on every update, and any value that genuinely changes still propagates. It also assumes the Rules of React hold, and offers a `"use no memo"` opt-out for code it cannot prove safe. So the compiler lowers the cost of the memo option; it does not reorder the diagnosis. ## A policy a team can actually follow 1. **Measure before changing anything.** Record a profile and identify which state owner is dragging expensive descendants into its commits. "It feels slow" is not a location. 2. **Ask the structural question first**: can this state live lower, can this component be split, can this subtree be relayed as content? 3. **If a memo boundary is still needed, require a written reason** in the code review — which parent re-renders, which props are stable, why they will stay stable. 4. **Place boundaries at few, high-fanout points**, not on every component in a folder. 5. **Re-examine boundaries when structure changes.** A memo that guarded against a parent that no longer re-renders is dead weight, and pre-production code should delete it rather than keep it. ## The failure mode you are guarding against Memo-everywhere codebases are not usually slower in a way anyone can point at — they are *unanalysable*. Every component compares props on every render, no one knows which boundaries still work, and the next performance problem cannot be reasoned about because the tree no longer tells you what re-renders. Keeping memoization rare is partly a performance decision and largely a diagnosability one.

  • A profile shows one component is slow to render on its own, not because an ancestor re-rendered. Does either lever help?
    Neither, directly — the cost is inside a single render, so skipping re-renders is the wrong axis. Look at the work in the body: expensive computation to precompute or cache on stable inputs, or too many elements produced at once, which is a windowing or pagination problem rather than a memoization one.
  • How do you stop "structure first" from becoming dogma that blocks reasonable memoization?
    Tie it to measurement, not principle. The rule is that a memo boundary needs a stated reason — which parent re-renders, which props are stable, why they will stay stable — and once someone can state that, the boundary is approved. What gets rejected is memo added speculatively, before anyone has located the actual state owner.
  • With the React Compiler enabled, is hand-placed React.memo obsolete?
    Much of the routine hand memoization stops being worth writing, since the compiler caches values and element objects at build time. But it cannot move state to a better owner, and it assumes the Rules of React hold, with a `"use no memo"` escape for code it cannot prove safe. Structural decisions and any boundary the compiler opted out of remain yours.

saying these in an interview costs you the question

  • Treats memo everywhere as a free default with no downside
  • Ignores that memo fails silently once a prop becomes unstable
  • Adds memoization before profiling to locate the state owner
  • Thinks the React Compiler decides where state should live
  • Keeps obsolete memo boundaries because removing them feels risky

context