Chapter 35: React Performance Optimization
React is fast by default. When it is not, the fix is usually surgical — a targeted optimization at a specific bottleneck, not a global rewrite. This chapter covers the tools and techniques for identifying and fixing performance issues.
Understanding Re-renders
A React component re-renders when:
- Its state changes (
useState,useReducer) - Its parent re-renders (unless the child is memoized)
- A context it consumes changes
- A hook it uses triggers a re-render
Not all re-renders are bad. React's reconciliation is fast. Only optimize when you can measure a problem.
Profiling Before Optimizing
Rule: Never optimize without measuring first. Premature optimization adds complexity without measurable benefit.
React DevTools Profiler
- Install React DevTools browser extension
- Open DevTools → Profiler tab
- Click "Record", interact with your app, click "Stop"
- Analyze the flame chart: which components took longest? Which re-rendered unnecessarily?
Web Vitals
// shared/lib/web-vitals.ts
import { onCLS, onINP, onLCP, onFCP, onTTFB } from "web-vitals";
export function reportWebVitals() {
onCLS(console.log); // Cumulative Layout Shift
onINP(console.log); // Interaction to Next Paint (replaced FID)
onLCP(console.log); // Largest Contentful Paint
onFCP(console.log); // First Contentful Paint
onTTFB(console.log); // Time to First Byte
}
React.memo: Prevent Unnecessary Re-renders
// ✅ Memo a component that receives the same props often
const ProjectCard = React.memo(function ProjectCard({ project }: { project: Project }) {
return (
<Card>
<CardHeader>{project.name}</CardHeader>
<CardContent>{project.description}</CardContent>
</Card>
);
});
// The component will only re-render if `project` reference changes
// Shallow comparison by default
When to use React.memo:
- Component renders frequently with the same props
- Component is expensive to render (large DOM tree, complex calculations)
- Component is in a list that re-renders when any item changes
When NOT to use React.memo:
- Component always receives different props (memo overhead with no benefit)
- Component is cheap to render (overhead of comparison exceeds render cost)
- You are guessing rather than measuring
useMemo and useCallback
// useMemo: cache expensive computations
function ProjectAnalytics({ tasks }: { tasks: Task[] }) {
const stats = useMemo(() => ({
total: tasks.length,
completed: tasks.filter((t) => t.status === "DONE").length,
overdue: tasks.filter((t) => t.dueDate && t.dueDate < new Date() && t.status !== "DONE").length,
byPriority: Object.groupBy(tasks, (t) => t.priority),
}), [tasks]); // Only recompute when tasks array changes
return <StatsGrid stats={stats} />;
}
// useCallback: stabilize function references for memoized children
function ProjectList({ projects }: { projects: Project[] }) {
const handleSelect = useCallback((projectId: string) => {
navigate({ to: "/projects/$projectId", params: { projectId } });
}, [navigate]);
return (
<div>
{projects.map((project) => (
<ProjectCard
key={project.id}
project={project}
onSelect={handleSelect} // Stable reference — memo'd ProjectCard won't re-render
/>
))}
</div>
);
}
Concurrent Features
useTransition: Non-Urgent Updates
function ProjectSearch() {
const [search, setSearch] = useState("");
const [isPending, startTransition] = useTransition();
const handleChange = (value: string) => {
// Urgent: update the input immediately
setSearch(value);
// Non-urgent: update the results list (can be interrupted)
startTransition(() => {
setFilteredProjects(filterProjects(allProjects, value));
});
};
return (
<div>
<Input value={search} onChange={(e) => handleChange(e.target.value)} />
<div className={isPending ? "opacity-60" : ""}>
<ProjectList projects={filteredProjects} />
</div>
</div>
);
}
useDeferredValue: Defer Expensive Renders
function SearchResults({ query }: { query: string }) {
// Defer the query value — React keeps showing old results while computing new ones
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
const results = useMemo(
() => expensiveSearch(deferredQuery),
[deferredQuery]
);
return (
<div className={isStale ? "opacity-50 transition-opacity" : ""}>
{results.map((result) => (
<ResultItem key={result.id} result={result} />
))}
</div>
);
}
Context Optimization
React Context re-renders ALL consumers when the value changes. Optimize by splitting contexts:
// ❌ One context for everything — every consumer re-renders on any change
const AppContext = createContext({
user: null,
theme: "light",
notifications: [],
sidebarOpen: true,
});
// ✅ Split by update frequency
const AuthContext = createContext<User | null>(null); // Rarely changes
const ThemeContext = createContext<"light" | "dark">("light"); // Rarely changes
const NotificationContext = createContext<Notification[]>([]); // Changes often
// Use Zustand for sidebarOpen — it supports selectors
Virtualization for Long Lists
When rendering hundreds or thousands of items, virtualize:
import { useVirtualizer } from "@tanstack/react-virtual";
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 64,
overscan: 5,
});
return (
<div ref={parentRef} className="h-[400px] overflow-auto">
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative" }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<ListItem item={items[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}
Summary
- ✅ Profile before optimizing — use React DevTools and Web Vitals
- ✅
React.memofor components with stable props that render frequently - ✅
useMemofor expensive computations;useCallbackfor stable function references - ✅
useTransitionanduseDeferredValuefor responsive UIs during heavy updates - ✅ Split contexts by update frequency to prevent unnecessary re-renders
- ✅ Virtualization for rendering large lists efficiently