Topics
v5.0.0
On this page · sections

Memoization & React Compiler

Memoization skips redundant work in the render cycle — React.memo at the component layer, useMemo and useCallback at the hook layer. The React Compiler automates much of this at build time via compile-time dependency analysis.

API / ToolWhat it cachesLayerSkips
React.memoComponent render decisionComponentChild re-render if props equal
useMemoComputed valueHookRecomputation; stable ref
useCallbackFunction referenceHookNew function identity
React CompilerValues + functions where safeBuild timeManual memo boilerplate

React.memo

Definition

React.memo wraps a component and skips re-rendering when props are shallow-equal to the previous render.

'use client'; import { memo, useState } from 'react'; type RowProps = { id: string; label: string }; const ExpensiveRow = memo(function ExpensiveRow({ id, label }: RowProps) { return ( <tr> <td>{id}</td> <td>{label}</td> </tr> ); }); function Table({ rows }: { rows: RowProps[] }) { const [filter, setFilter] = useState(''); return ( <> <input value={filter} onChange={(e) => setFilter(e.target.value)} /> <table> <tbody> {rows.map((row) => ( <ExpensiveRow key={row.id} id={row.id} label={row.label} /> ))} </tbody> </table> </> ); }

Typing in filter re-renders Table but memoized rows skip — props unchanged. Custom compare is possible but easy to get wrong:

const Row = memo( function Row({ item }: { item: Item }) { return <li>{item.label}</li>; }, (prev, next) => prev.item.id === next.item.id, );

memo guards the child; the parent still renders. Context consumers bypass props comparison entirely.

Interview: Inline objects, arrays, and arrow functions create new references every render and defeat memo.

useMemo

Definition

useMemo(compute, deps) caches a computed value between renders. When deps are unchanged, it returns the same reference.

'use client'; import { memo, useMemo, useState } from 'react'; const TagSummary = memo(function TagSummary({ summary, }: { summary: Record<string, number>; }) { return <pre>{JSON.stringify(summary)}</pre>; }); function Inventory({ items }: { items: Item[] }) { const [query, setQuery] = useState(''); const filtered = useMemo(() => { const q = query.toLowerCase(); return items.filter((item) => item.tags.some((t) => t.toLowerCase().includes(q)), ); }, [items, query]); const tagCounts = useMemo(() => { const counts: Record<string, number> = {}; for (const item of filtered) { for (const tag of item.tags) { counts[tag] = (counts[tag] ?? 0) + 1; } } return counts; }, [filtered]); return ( <> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <TagSummary summary={tagCounts} /> </> ); }

Use for expensive pure calculations and stable object identity passed to memo children. useMemo must be pure — no side effects.

Interview: useMemo caches a value. React.memo skips a component render. Different layers.

useCallback

Definition

useCallback(fn, deps) returns the same function reference when deps are unchanged.

'use client'; import { memo, useCallback, useState } from 'react'; const ToolbarButton = memo(function ToolbarButton({ label, onPress, }: { label: string; onPress: () => void; }) { return ( <button type="button" onClick={onPress}> {label} </button> ); }); function Toolbar({ onSave }: { onSave: (payload: string) => void }) { const [draft, setDraft] = useState(''); const handleSave = useCallback(() => onSave(draft), [draft, onSave]); const handleClear = useCallback(() => setDraft(''), []); return ( <div> <input value={draft} onChange={(e) => setDraft(e.target.value)} /> <ToolbarButton label="Save" onPress={handleSave} /> <ToolbarButton label="Clear" onPress={handleClear} /> </div> ); }

Matters when passing callbacks to memo children or listing callbacks in effect deps. dispatch from useReducer is stable — no useCallback needed.

Interview: useCallback does not make the function faster — only its reference identity is stable.

What Defeats Memoization

Definition

function BrokenParent() { const [n, setN] = useState(0); const onClick = useCallback(() => setN((v) => v + 1), []); // new config every render — memo always fails return <Widget config={{ color: 'red' }} onClick={onClick} />; }

Common defeaters:

Inline object literal → <Child style={{ color: 'red' }} /> Inline array → <Child items={[a, b]} /> Inline arrow function → <Child onClick={() => doWork()} /> Unstable context value → memo on consumer is wasted

Fix order before reaching for memo:

1. Colocate state down 2. Split context 3. Pass expensive subtrees as children 4. Stabilize values with useMemo / useCallback 5. Then memo — where profiler implicates

Interview: If Profiler shows memo never bails out, remove it — compare tax with zero skip.

React Compiler

Definition

The React Compiler is a Babel plugin that analyzes component control flow at build time and inserts automatic memoization — caching values and stabilizing functions where dependencies are provably safe.

'use client'; import { useState } from 'react'; function SearchList({ items }: { items: string[] }) { const [query, setQuery] = useState(''); const filtered = items.filter((item) => item.toLowerCase().includes(query.toLowerCase()), ); const handleSelect = (id: string) => { console.log('selected', id); }; return ( <div> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <ul> {filtered.map((item) => ( <li key={item} onClick={() => handleSelect(item)}>{item}</li> ))} </ul> </div> ); } // With compiler: stable handleSelect + filtered cache when deps unchanged

Say automatic memoization with compile-time dependency analysis — not “it makes React faster magically.”

Rules of React for compiler safety

Components and hooks must be pure during render:

// ❌ Impure render — Date.now() drives UI function BadClock() { return <span>{Date.now()}</span>; } // ✅ State + effect function GoodClock() { const [now, setNow] = useState(() => Date.now()); useEffect(() => { const id = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(id); }, []); return <span>{now}</span>; }
Pure render → same props/state → same output; no fetch in render Hooks → top level only; no conditional hooks Immutable state → mutations hide dependency changes Refs → imperative escape; do not drive UI from ref mutations alone

Interview: Compiler targets client components and hooks. Server Components do not re-render — compiler focus is client interactivity.

Compiler vs Manual Memo

Definition

Remove redundant manual memo once compiler is on and ESLint is clean. Keep manual memo when you need semantic guarantees the compiler cannot prove.

// Keep — expensive derived data with explicit contract function useStableSnapshot(data: BigObject) { return useMemo(() => ({ count: Object.keys(data.nodes).length }), [data]); } // Delete after compiler — ceremonial useCallback function HandMemoButton({ onClick, label }: { onClick: () => void; label: string }) { const stableClick = useCallback(onClick, [onClick]); return <button onClick={stableClick}>{label}</button>; }
Remove → redundant useCallback/useMemo mirroring compiler output Keep → custom equality, library interop, documented public API stability Still required → virtualization, code splitting, children composition, Context split

Opt out surgically with "use no memo" per module or component. Use eslint-plugin-react-compiler in CI.

Interview: Compiler does not fix Context broadcast — split contexts or use selectors. Compiler does not fix lists without keys.

When Memoization Hurts

Definition

False security → memo with unstable props = compare tax + no skip Stale closures → useCallback missing dep → old state in handler Garbage retention → large memoized objects stay alive while deps unchanged Premature memo → profile before adding

Event handlers are often cheap to recreate — useCallback only when measured need exists. memo on a context consumer is wasted — context updates bypass props comparison.

Interview: Architecture fixes beat memo carpet — colocate state, split context, virtualize lists.

Senior Interview Summary

What to say

React.memo skips child renders on shallow-equal props. useMemo caches values; useCallback stabilizes function references. The React Compiler inserts automatic memoization at build time for pure components — remove redundant manual memo in compiled paths, keep it for library interop and custom equality.

Best mental model

React.memo → "Skip rendering this component." useMemo → "Remember this computed value." useCallback → "Remember this function reference." Compiler → "Auto memo where analysis is safe."

Practical rule

Profile first → memo without evidence is overhead Inline object defeats memo → fix parent, not band-aid useMemo Context consumer + memo → wasted; split context instead Expensive pure derive → useMemo Callback to memo child → useCallback Scroll jank on 10k rows → virtualize, not more memo Enable compiler per package → ESLint gate → leaf packages → INP metrics

Last updated on