Wrap expensive calculations in useMemo and expensive component creation in React.memo. Without memoization, expensive work runs on every render even when inputs haven't changed, causing UI jank and dropped frames.
Why This Matters
Every React re-render executes the entire component function body. If that body contains an O(n) sort, a complex filter, or a data transformation, that work runs on every keystroke, scroll event, and parent re-render — even when the data hasn't changed. This causes visible frame drops and UI lag, especially on lower-powered devices.
React components are functions that run on every render. If your component performs an expensive computation — sorting a large array, building a tree structure, running a complex filter — that computation executes every single time the component renders, regardless of whether its inputs changed.
On a fast development machine, you might not notice. But on a mid-range phone rendering at 60fps, a 16ms frame budget means even a 5ms computation can cause dropped frames. When multiple components each do unnecessary work, the jank becomes obvious.
Memoization tells React to cache the result and reuse it when the inputs (dependencies) haven't changed. It's not a premature optimization — it's preventing unnecessary work.
The rule
Use useMemo for expensive computations (sorting, filtering, transforming large datasets). Use React.memo for components that receive the same props frequently but whose parent re-renders often. Always measure first — don't memoize trivial computations.
Bad example
function AnalyticsDashboard({ events }: { events: AnalyticsEvent[] }) { // BAD: sorts and groups 10,000 events on every render const groupedByDay = events .sort((a, b) => a.timestamp - b.timestamp) .reduce((groups, event) => { const day = new Date(event.timestamp).toDateString(); (groups[day] ??= []).push(event); return groups; }, {} as Record<string, AnalyticsEvent[]>); const dailyTotals = Object.entries(groupedByDay).map(([day, dayEvents]) => ({ day, total: dayEvents.length, uniqueUsers: new Set(dayEvents.map((e) => e.userId)).size, })); return <Chart data={dailyTotals} />;}
Good example
function AnalyticsDashboard({ events }: { events: AnalyticsEvent[] }) { // GOOD: only recomputes when events array changes const dailyTotals = useMemo(() => { const groupedByDay = events .sort((a, b) => a.timestamp - b.timestamp) .reduce((groups, event) => { const day = new Date(event.timestamp).toDateString(); (groups[day] ??= []).push(event); return groups; }, {} as Record<string, AnalyticsEvent[]>); return Object.entries(groupedByDay).map(([day, dayEvents]) => ({ day, total: dayEvents.length, uniqueUsers: new Set(dayEvents.map((e) => e.userId)).size, })); }, [events]); return <Chart data={dailyTotals} />;}
How to detect
Profile your application with React DevTools Profiler. Look for components that:
Take >1ms to render
Re-render frequently
Perform sorting, filtering, or data transformations in the function body without useMemo