Performance — memo, useMemo & useCallback
First: Re-Renders Are Usually Fine
When state changes, the component and all its descendants re-render. That sounds wasteful but rendering is cheap — React diffs the output and touches only changed DOM. Optimise when you measure a problem, not preemptively: premature memoisation clutters code for zero felt benefit.
Measure with React DevTools → Profiler: record an interaction, see which components rendered and how long they took. Long bars or huge render counts = your targets.
React.memo — Skip Unchanged Children
Wrap a component and it skips re-rendering when its props are shallow-equal:
const CourseCard = React.memo(function CourseCard({ course, onSelect }) {
return (
<article onClick={() => onSelect(course.id)}>
<h3>{course.title}</h3>
</article>
);
});Classic use: a 500-row list where typing in an unrelated search box re-renders every row. Memoise the row, and only rows whose props changed re-render.
The Catch: Reference Equality
Memo compares props with ===. Objects and functions created during render are new every time — silently defeating memo:
// every render creates a new function → CourseCard re-renders anyway
<CourseCard course={course} onSelect={(id) => setSelected(id)} />That’s what the other two hooks fix.
useCallback — Stable Functions
const handleSelect = useCallback((id) => {
setSelected(id);
}, []); // same function object across renders
<CourseCard course={course} onSelect={handleSelect} /> // memo now worksuseMemo — Cache Computations & Objects
// expensive derived data — recompute only when inputs change
const ranked = useMemo(
() => [...courses].sort((a, b) => b.rating - a.rating),
[courses]
);
// stable object identity for a memoised child or context value
const contextValue = useMemo(() => ({ user, login, logout }), [user]);That second use matters for Context: an inline value={{ ... }} re-renders every consumer on every provider render.
Cheaper Wins First
- Push state down — if only the search box uses
query, hold the state in the search box, not the page. Smaller blast radius beats memoising the world. - Children as props escape re-renders —
<Layout>{children}</Layout>: when Layout’s own state changes, the children element (created by the parent) is unchanged and skipped. - Keys for list identity — wrong keys cause unnecessary unmount/remount churn.
- Lazy-load routes —
const Admin = React.lazy(() => import("./Admin"))with<Suspense>keeps the initial bundle small.
(The React Compiler, now stabilising, auto-memoises components — but it rewards exactly the patterns above: pure renders, immutable updates. Write idiomatic React and the tooling keeps making it faster.)