Performance Optimization

Roughly 12% of a typical loop — and 12% of the mock exam here.

Memoization — and how it silently fails

memo skips re-rendering a component when its props are shallow-equal, compared with Object.is. The classic trap: a candidate wraps a component in memo, then passes an inline object or arrow function. Every render creates a fresh reference, the comparison fails, and the memo does nothing — while still charging for the comparison.

// memo defeated: both props are new references every render
<MemoizedRow style={{ padding: 8 }} onSelect={() => select(id)} />

The fix is useMemo for object and array props, useCallback for functions — but know the real cost-benefit. Every memoization hook adds a dependency comparison on every render plus retained memory, and it clutters the code. Per react.dev, useMemo earns its place only when the computation is measurably slow and its dependencies rarely change, or when the value feeds a memo-wrapped child or another hook’s dependency array. “Memoize everything” is the trap answer; “profile, then memoize proven hotspots” is the rule that resolves it.

The React Compiler changed the default answer

React Compiler 1.0 shipped stable in October 2025. It auto-applies the equivalent of memo, useMemo, and useCallback at build time — including intermediate values that manual memoization misses. It works best with React 19 and also supports 17 and 18. In a compiled codebase, hand-written memoization is mostly obsolete.

What it does not obsolete: architecture. The compiler cannot fix a slow algorithm, an oversized bundle, or state lifted too high. A strong 2026 answer names the compiler and its limits — colocating state, passing JSX as children, and keeping renders pure still matter.

Ship less, render less

Two levers that beat any memo:

  • Code splittinglazy(() => import('./Chart')) plus a Suspense boundary with a fallback loads a route or heavy widget on demand instead of in the main bundle. Split at routes first; that is where the payoff is largest.
  • List virtualization — for long lists, render only the visible window (react-window, TanStack Virtual). A 10,000-row table becomes roughly 20 mounted rows. No memoization competes with not rendering.

Related trap: key={index} on a list that reorders or inserts. React matches children by key, so row state and memo caches stick to the wrong items and reconciliation degrades into churn. Keys must be stable identity, unique among siblings.

Measure first: Profiler and INP

The React DevTools Profiler records commits and shows a flame graph of what rendered, how long it took, and — with “record why each component rendered” enabled — which prop or state change caused it. React 19.2 also added Scheduler and Components tracks to Chrome DevTools performance profiles, so React’s priority lanes show up alongside browser work.

Connect it to Web Vitals: INP (Interaction to Next Paint) measures how long the page takes to paint after an interaction. A long synchronous render blocks that paint, so slow React renders are an INP problem. Marking non-urgent updates with startTransition or useDeferredValue keeps typing and clicks responsive while heavy renders proceed at lower priority.

Effect chains are a performance bug

A chain of useEffect hooks where each one sets state that triggers the next causes cascading render passes — three chained effects can mean three full extra renders per change. The react.dev rule: if a value can be computed from props or state, derive it during render (memoized if expensive) instead of mirroring it into state via an effect. Reserve effects for synchronizing with external systems, and use React 19.2’s useEffectEvent to keep incidental values from re-running an effect at all.

Sample questions

6 of the 24 questions this domain carries in practice mode — expand one to check yourself before drilling.

1. Wrapping a component in memo changes its rendering behavior in exactly one way:
  1. it caches the component's JSX output permanently, so later prop changes are ignored until the component remounts
  2. it moves the component's rendering onto a background thread so the main thread stays free
  3. it skips the re-render when a parent re-renders but the new props are shallowly equal to the old ones
  4. it batches the component's state updates so at most one render happens per animation frame

Answer: C. memo compares props shallowly; renders triggered by the component's own state or by context it subscribes to still happen regardless of the wrapper.

2. A child wrapped in memo re-renders every time its parent does, even though its props look identical in the JSX. The parent passes style={{ margin: 8 }}. The most likely cause is:
  1. the object literal creates a new reference on each parent render, so memo's shallow comparison always fails
  2. memo only works on class components, so the wrapper is silently ignored for this function component
  3. style props are exempt from React's memoization because the browser layout engine, not React, owns everything CSS-related
  4. the parent needs a key prop on the child before React is allowed to reuse its output

Answer: A. Shallow comparison is reference equality for objects; hoist the literal or memoize it to restore the bailout — inline arrow functions defeat memo the same way.

3. A team wraps every function in every component in useCallback "for performance." The most accurate assessment is:
  1. this is best practice — recreating functions on each render is the dominant rendering cost in most React apps
  2. useCallback was deprecated in React 19 in favor of the compiler, so every wrapper now logs a warning
  3. it is harmless either way, because hooks like useCallback compile down to zero-cost no-ops in optimized production builds
  4. most wrappers buy nothing — useCallback only helps when the function reaches a memoized child or a dependency array

Answer: D. Every useCallback still allocates the function and compares deps each render, so blanket wrapping adds overhead without skipping a single render by itself.

4. A memoized Layout component re-renders whenever its parent does, even though its only prop is children. The reason is:
  1. the JSX passed as children is a fresh element object on every parent render, failing the shallow check
  2. children is a reserved prop that React requires memo to skip whenever it compares old and new props
  3. memo compares props deeply, and deep comparison always reports JSX trees as changed
  4. Layout is missing a displayName, which quietly disables memoization everywhere outside development builds

Answer: A. JSX children are freshly created element objects, exactly like inline props; hoist the subtree or accept it from a rarely-rendering ancestor to keep the bailout.

5. When the React Compiler encounters a component that violates the Rules of React — say, it mutates a prop during render — the compiler:
  1. auto-fixes the violation by defensively cloning the mutated object before the render continues
  2. compiles the component anyway, and the prop mutation silently disappears from production behavior
  3. fails the entire build with a hard error until the offending component is found and rewritten
  4. skips optimizing that component, leaving it exactly as written — behavior unchanged, just unmemoized

Answer: D. Bail-outs are silent by design, so the compiler-powered rules in eslint-plugin-react-hooks are how teams discover which components were left unoptimized and why.

6. After adding a custom comparison function with memo(List, areEqual), the list no longer updates when its items change. Reviewing areEqual, the likely bug is:
  1. it returns true when props differ — memo skips rendering on true, the opposite of shouldComponentUpdate
  2. custom comparator functions must be declared async in React 19, and a synchronous one always misfires
  3. memo silently ignores custom comparators whenever the component receives more than one prop
  4. areEqual only runs in development builds, so production comparisons silently fall back to reference checks

Answer: A. arePropsEqual answers "may we skip this render?", the exact inverse of shouldComponentUpdate — porting class-era logic without flipping the boolean freezes the UI.

Drill this domain in practice mode →

Independent community study resource — not affiliated with or endorsed by Meta Platforms, Inc. React is a trademark of Meta Platforms, Inc. All questions and study notes are original, written from the official React documentation. Everything runs in your browser; nothing you answer is stored or transmitted.