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.
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.
BeforeMerge scans your pull requests against this rule and 4+ others. Get actionable feedback before code ships.
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.
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.
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} />;
}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} />;
}Profile your application with React DevTools Profiler. Look for components that:
useMemogrep -n "\.sort\|.filter\|.reduce\|.map" --include="*.tsx" -r src/Then check if the result is wrapped in useMemo.
useMemo with correct dependenciesReact.memo if they receive stable props but re-render due to parent updatesuseCallback for functions passed as props to memoized children