Performance — Cheat Sheet
React · 4 topics. Download the PDF or the Instagram carousel and share it.
Cheat Sheet · AiCanCode.org
Performance
React4 topicsQuick revision reference
1
Diagnosing Unnecessary Re-renders
Find the cause before applying a fix. Nearly every "React is slow" report turns out to be one of four specific causes, and three of them are fixed by moving code rather than memoising it.
- ✓A re-render is only a problem when it is expensive or cascades widely — measure before optimising
- ✓The four causes are state too high, an unmemoised context value, an unstable prop defeating memo, and genuinely expensive work
- ✓Highlight-updates and the Profiler's "why did this render" identify the cause in seconds
- ✓Moving state down and passing children through fix most cases with no memoisation at all
- ✓StrictMode doubles development renders — verify counts in a production build, on a throttled mid-range device
Four causes, three structural fixes
// 1. State too high — a keystroke in a search box re-renders a
// 2,000-row table that does not read the query
function Page() {
const [query, setQuery] = useState('') // move this into SearchBox
return <><SearchBox value={query} onChange={setQuery} /><BigTable /></>
}
// Fix: move the state down, or pass BigTable as children.
// 2. Context value recreated every render
<Ctx.Provider value={{ user, signOut }}> // new object each time
// Fix: useMemo the value; split contexts.
// 3. Unstable prop defeating memo
<MemoRow onSelect={() => pick(id)} /> // new function each render
// Fix: useCallback, or pass the id and a stable handler.
// 4. Genuinely expensive render — a big list, a chart, a heavy
// markdown or syntax-highlighting pass
// Fix: useMemo the computation, virtualise the list, or split the
// work with useDeferredValue.
// Causes 1-3 are fixed by structure. Only 4 needs memoisation.2
Profiling and Measuring
Two different questions need two different tools: "which component is slow" is the React Profiler, "is the page slow for users" is Lighthouse and field metrics.
- ✓The React Profiler answers "which component is slow"; Lighthouse and field metrics answer "is the page slow"
- ✓Read a profile as: how many commits, largest self duration, and why did it render
- ✓A frame is 16ms — renders longer than that drop frames and show up as INP
- ✓Verify in a production build, since development React is slower and StrictMode doubles renders
- ✓For a slow-loading app, analyse the bundle first — the cause is usually one oversized dependency on the critical path
Commits, self duration, and why
// React DevTools -> Profiler -> record -> interact -> stop
// The flame chart shows, per commit:
// - which components rendered
// - actual duration (this component and its children)
// - self duration (this component alone)
// - WHY it rendered, with "Record why each component rendered" on
// Read it in this order:
// 1. How many commits did one interaction cause? Several is a
// cascade — usually chained effects or state set during render.
// 2. Which component has the largest self duration?
// 3. Why did it render — props, state, hooks, or parent?
// The Profiler API for production measurements
<Profiler id="ProblemList" onRender={(id, phase, actual, base) => {
if (actual > 16) analytics.track('slow_render', { id, phase, actual })
}}>
// 16ms is one frame at 60fps. Anything longer drops a frame.
// Always confirm in a production build: development React is far
// slower, and StrictMode doubles the render count.3
Shipping Less JavaScript
The fastest code is the code you never send. Splitting, deferring and choosing smaller dependencies beat almost any runtime optimisation.
- ✓Splitting by route is the highest-value change; then heavy libraries, below-the-fold content and conditional features
- ✓Stop splitting when the extra round trip costs more than the bytes saved
- ✓fetch, Intl and crypto.randomUUID have replaced several once-standard dependencies
- ✓Third-party scripts often outweigh the app bundle — load them after interactive
- ✓Virtualisation renders only visible rows and is worth it above roughly 200 items, at the cost of in-page find
Route, heavy library, below the fold, conditional
// 1. By route — the biggest single win
const Admin = lazy(() => import('./features/admin/AdminPage'))
// 2. Heavy libraries, loaded on demand
async function exportPdf(rows) {
const { jsPDF } = await import('jspdf') // 300KB, only on click
...
}
// 3. Below the fold — comments, recommendations, related items
const Comments = lazy(() => import('./Comments'))
<Suspense fallback={<CommentsSkeleton />}>
{inView && <Comments />} // with an observer
</Suspense>
// 4. Conditional features — an editor only Pro users open,
// an admin toolbar, a debug panel
// Where to stop: a small component on the critical path. An extra
// round trip costs more than the few kilobytes saved, and too many
// chunks is its own problem.4
Perceived Performance
How fast an interface feels is a separate problem from how fast it is. Responding immediately, showing structure early and never moving content account for most of it.
- ✓Acknowledge every interaction within 100ms even when the work takes longer
- ✓Optimistic updates and prefetch-on-hover can make an action feel instantaneous
- ✓Stream content in parts with separate Suspense boundaries rather than waiting for everything
- ✓Show cached or partial data immediately and fill in the rest, instead of clearing to a skeleton
- ✓Reserve space for late-loading content and delay spinners, because layout shift and flashing feel worse than waiting
100ms, 1s, 10s
// < 100ms feels instant — no feedback needed
// < 300ms noticeable — a subtle state change suffices
// < 1s the flow of thought holds — show a spinner or skeleton
// > 1s attention drifts — show progress and what is happening
// > 10s they leave — show progress, an estimate, and a way to cancel
// So: acknowledge EVERY interaction within 100ms, even if the work
// takes longer. The button changes state on click, not on response.
<button onClick={run} disabled={isRunning}>
{isRunning ? 'Running…' : 'Run'} // instant acknowledgement
</button>
// Optimistic updates put the result on screen immediately for
// actions that almost always succeed — a like, a toggle, a reorder.
// A perceived 0ms, with a rollback if the server disagrees.
// Prefetch on intent: hovering a link starts the request ~250ms
// before the click, which is often the entire request time.Learn this free with Aria, your AI tutor → AiCanCode.org/learn/react