Course outline

React / Performance and trustworthy boundaries / Lesson 14

Profiling and deliberate memoization

Measure a representative interaction, locate expensive work, and use identity-sensitive optimizations only where they improve the observed contract.

About 4 min readContent reviewed
On this page

Lesson goals

  • Define the performance question
  • Memoize a pure derived calculation
  • Validate gains and retained resources

Define the performance question

Performance work starts with a user-visible metric such as typing responsiveness, navigation latency, or time until useful content. Record the runtime, build mode, device constraints, and dataset. Development instrumentation and Strict Mode can distort timings, so use an appropriate profiling or production measurement arrangement. React rendering is only one part of the page: network delay, JavaScript calculations, layout, image decoding, and painting can dominate. A component that renders often is not necessarily expensive, and a component that renders once can still block the main thread. Measure before optimizing the part whose source merely looks complicated.

Identity is a contract with tradeoffs

memo can often skip a parent-driven render when props compare equal under its comparison rules, but it is an optimization rather than a semantic guarantee. A memoized component can still update from its own state or consumed context. New object and function props can defeat a shallow identity comparison. Before adding caches, simplify prop shape and state ownership. A custom comparator must preserve both output and behavior, including callbacks with captured values; comparing only visible text can retain a stale handler. Its comparison cost can exceed the render it avoids, so keep the measured workload in view.

Memoize a pure derived calculation

The first example uses useMemo around a filter and a reduction over a stable local dataset. The example is intentionally small, so its result is meaningful but it makes no speed claim. Dependencies include the query actually read. useMemo caches a calculation result under React’s rules; it is not durable storage and should not be needed for correctness. The calculation must remain pure if repeated. Do not cache a network mutation or rely on the callback running exactly once. If removing useMemo changes the application’s meaning, the optimization is hiding a state or side-effect design defect.

jsx Component module — requires the course React host
import { useMemo, useState } from "react";
const items = [{ id: "a", label: "Alpha", cost: 2 }, { id: "b", label: "Beta", cost: 3 }];
export default function App() { const [query, setQuery] = useState(""); const total = useMemo(() => items.filter(x => x.label.toLowerCase().includes(query.toLowerCase())).reduce((n, x) => n + x.cost, 0), [query]); return <main><h1>Filtered total: {total}</h1><label>Query<input value={query} onChange={e => setQuery(e.target.value)} /></label></main>; }
Expected output
Initial total: 5. Query bet: total 3.

The calculation is pure and depends on query; removing the cache would preserve its result. The fixed dataset keeps the example independently checkable.

Keep stable props only where useful

The second example wraps a small display component in memo and passes a primitive label unaffected by an unrelated counter. It illustrates the prop boundary without logging render counts as a public result. useCallback can cache a function identity when that identity matters to a measured boundary, but it does not make an impure callback correct or avoid its eventual execution. Current React Compiler tooling can automate some memoization when explicitly installed and configured; using a current React version alone does not prove the compiler is enabled. Keep the build assumption explicit instead of recommending manual memoization everywhere or promising automatic optimization everywhere.

jsx Component module — requires the course React host
import { memo, useState } from "react";
const Label = memo(function Label({ text }) { return <p>{text}</p>; });
export default function App() { const [count, setCount] = useState(0); return <main><h1>Stable prop boundary</h1><Label text="Fixed label" /><button type="button" onClick={() => setCount(c => c + 1)}>Unrelated count {count}</button></main>; }
Expected output
The label stays Fixed label while the button count increases.

The child’s primitive prop is unchanged by counter updates. Memoization may skip work, but the visible contract does not depend on a render count.

Validate gains and retained resources

After one targeted change, repeat the same interaction and compare the metric plus memory and complexity costs. Remove ineffective caches because they add dependency and identity obligations. For long lists, consider data reduction, pagination, or a reviewed virtualization approach before retaining every row indefinitely; virtualization also needs keyboard and accessibility design. Inspect leaked subscriptions and retained closures if memory grows across repeated navigation. Keep functional and interaction tests alongside profiling so a faster result still means the same thing. A benchmark that drops rows or ignores errors is measuring a different behavior, not an optimization of the original task.

Common errors and fixes

Memoization hides a correctness problem

Why it happens
A cached calculation performs side effects or owns hidden state.
How to fix it
Restore purity and explicit ownership first.
Prevent it next time
Verify behavior with the optimization removed.

A comparator retains an old callback

Why it happens
It compared presentation props but ignored behavior.
How to fix it
Use correct complete semantics or remove the custom comparator.
Prevent it next time
Test interactions after prop changes.

An upgrade is assumed to enable the compiler

Why it happens
Runtime version and build tooling were conflated.
How to fix it
Inspect the actual compiler configuration.
Prevent it next time
Record optimization assumptions with measurements.

Check your understanding

1. Why can a new object prop make memo ineffective even if its fields look unchanged?

Show worked solution

The default comparison uses prop identity semantics, so a newly created object is different. First ask whether the child can accept the actual primitive values it needs or whether the allocation belongs elsewhere. Add identity caching only when a measured boundary benefits and dependencies remain correct.

2. A profiling change halves time by omitting error rows. Is it a valid optimization?

Show worked solution

No, unless the contract explicitly changed to exclude them. Preserve all required results and recovery behavior before comparing performance. Otherwise the benchmark measures less work for a different product outcome and cannot justify the change as a faithful speedup.

Key takeaways

  • Measure before memoizing.
  • Caches are optional performance tools.
  • Preserve behavior and include resource costs.

Official references