RE
Performance
React advanced v1.0.0
React Performance Optimization
Overview
React performance optimization involves minimizing unnecessary re-renders, efficient data handling, and proper use of memoization. This skill covers profiling, React.memo, useMemo, useCallback, virtualization, and code splitting.
Key Concepts
React Rendering Process
┌─────────────────────────────────────────────────────────────┐
│ React Render Cycle │
├─────────────────────────────────────────────────────────────┤
│ │
│ Trigger ──▶ Render ──▶ Reconciliation ──▶ Commit ──▶ Paint │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Trigger Causes: │ │
│ │ • State change (setState) │ │
│ │ • Props change (parent re-render) │ │
│ │ • Context change │ │
│ │ • Force update │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Optimization Strategies: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Reduce Renders: │ │
│ │ ├── React.memo for pure components │ │
│ │ ├── useMemo for expensive computations │ │
│ │ ├── useCallback for stable function references │ │
│ │ └── Context splitting to reduce subscribers │ │
│ │ │ │
│ │ Reduce Work per Render: │ │
│ │ ├── Virtualize long lists │ │
│ │ ├── Lazy load components │ │
│ │ ├── Defer non-critical updates │ │
│ │ └── Use CSS instead of JS for animations │ │
│ │ │ │
│ │ Reduce Bundle Size: │ │
│ │ ├── Code splitting │ │
│ │ ├── Tree shaking │ │
│ │ └── Dynamic imports │ │
│ │ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Best Practices
1. Profile Before Optimizing
Use React DevTools Profiler to identify actual bottlenecks.
2. Don’t Memoize Everything
Memoization has overhead; only memoize when needed.
3. Keep Component Trees Shallow
Deep nesting increases reconciliation work.
4. Use Stable References
Prevent unnecessary re-renders with stable props.
5. Virtualize Large Lists
Only render visible items.
Code Examples
Example 1: React.memo with Custom Comparison
import React, { memo, useState, useCallback } from 'react';
// Heavy component that should only re-render when needed
interface UserCardProps {
user: User;
onSelect: (id: string) => void;
isSelected: boolean;
}
// Without memo - re-renders whenever parent re-renders
function UserCard({ user, onSelect, isSelected }: UserCardProps) {
console.log(`Rendering UserCard for ${user.id}`);
return (
<div
className={`user-card ${isSelected ? 'selected' : ''}`}
onClick={() => onSelect(user.id)}
>
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}
// With memo - only re-renders when props actually change
const MemoizedUserCard = memo(UserCard);
// Custom comparison for complex props
const MemoizedWithCustomCompare = memo(
UserCard,
(prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return (
prevProps.user.id === nextProps.user.id &&
prevProps.user.updatedAt === nextProps.user.updatedAt &&
prevProps.isSelected === nextProps.isSelected &&
prevProps.onSelect === nextProps.onSelect
);
}
);
// Parent component
function UserList({ users }: { users: User[] }) {
const [selectedId, setSelectedId] = useState<string | null>(null);
// ❌ BAD: Creates new function on every render
// const handleSelect = (id: string) => setSelectedId(id);
// ✅ GOOD: Stable function reference
const handleSelect = useCallback((id: string) => {
setSelectedId(id);
}, []);
return (
<div className="user-list">
{users.map((user) => (
<MemoizedUserCard
key={user.id}
user={user}
onSelect={handleSelect}
isSelected={user.id === selectedId}
/>
))}
</div>
);
}
Example 2: useMemo and useCallback
import { useMemo, useCallback, useState } from 'react';
interface DataTableProps {
data: Item[];
filters: Filters;
sortConfig: SortConfig;
}
function DataTable({ data, filters, sortConfig }: DataTableProps) {
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
// ✅ Expensive computation - memoize the result
const processedData = useMemo(() => {
console.log('Processing data...');
// Filter
let result = data.filter((item) => {
if (filters.search && !item.name.includes(filters.search)) {
return false;
}
if (filters.category && item.category !== filters.category) {
return false;
}
return true;
});
// Sort
result = [...result].sort((a, b) => {
const aVal = a[sortConfig.key];
const bVal = b[sortConfig.key];
const direction = sortConfig.direction === 'asc' ? 1 : -1;
if (typeof aVal === 'string') {
return aVal.localeCompare(bVal as string) * direction;
}
return ((aVal as number) - (bVal as number)) * direction;
});
return result;
}, [data, filters, sortConfig]); // Only recompute when these change
// ✅ Stable callback - prevents child re-renders
const toggleRow = useCallback((id: string) => {
setExpandedRows((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
}, []);
// ✅ Derived state - simple calculation, no need for useMemo
const totalValue = processedData.reduce((sum, item) => sum + item.value, 0);
// ❌ DON'T memoize simple values
// const isFiltered = useMemo(() => !!filters.search, [filters.search]);
// ✅ Just compute it
const isFiltered = !!filters.search || !!filters.category;
return (
<table>
<tbody>
{processedData.map((item) => (
<TableRow
key={item.id}
item={item}
isExpanded={expandedRows.has(item.id)}
onToggle={toggleRow}
/>
))}
</tbody>
<tfoot>
<tr>
<td>Total: {totalValue}</td>
</tr>
</tfoot>
</table>
);
}
// Memoized row component
const TableRow = memo(function TableRow({
item,
isExpanded,
onToggle,
}: {
item: Item;
isExpanded: boolean;
onToggle: (id: string) => void;
}) {
return (
<>
<tr onClick={() => onToggle(item.id)}>
<td>{item.name}</td>
<td>{item.value}</td>
</tr>
{isExpanded && (
<tr>
<td colSpan={2}>{item.details}</td>
</tr>
)}
</>
);
});
Example 3: Virtual List
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef, useMemo } from 'react';
interface VirtualListProps {
items: Item[];
itemHeight: number;
}
function VirtualList({ items, itemHeight }: VirtualListProps) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => itemHeight,
overscan: 5, // Render 5 extra items above/below viewport
});
const virtualItems = virtualizer.getVirtualItems();
return (
<div
ref={parentRef}
style={{
height: '400px',
overflow: 'auto',
}}
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{virtualItems.map((virtualItem) => {
const item = items[virtualItem.index];
return (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<ItemRow item={item} />
</div>
);
})}
</div>
</div>
);
}
// Virtual grid for 2D layouts
function VirtualGrid({ items, columns }: { items: Item[]; columns: number }) {
const parentRef = useRef<HTMLDivElement>(null);
const rowCount = Math.ceil(items.length / columns);
const rowVirtualizer = useVirtualizer({
count: rowCount,
getScrollElement: () => parentRef.current,
estimateSize: () => 200,
overscan: 2,
});
const columnVirtualizer = useVirtualizer({
horizontal: true,
count: columns,
getScrollElement: () => parentRef.current,
estimateSize: () => 200,
overscan: 2,
});
return (
<div
ref={parentRef}
style={{
height: '600px',
width: '100%',
overflow: 'auto',
}}
>
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
width: `${columnVirtualizer.getTotalSize()}px`,
position: 'relative',
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) =>
columnVirtualizer.getVirtualItems().map((virtualColumn) => {
const index = virtualRow.index * columns + virtualColumn.index;
const item = items[index];
if (!item) return null;
return (
<div
key={`${virtualRow.index}-${virtualColumn.index}`}
style={{
position: 'absolute',
top: 0,
left: 0,
width: `${virtualColumn.size}px`,
height: `${virtualRow.size}px`,
transform: `translateX(${virtualColumn.start}px) translateY(${virtualRow.start}px)`,
}}
>
<GridItem item={item} />
</div>
);
})
)}
</div>
</div>
);
}
Example 4: Code Splitting and Lazy Loading
import { lazy, Suspense, useState, useTransition } from 'react';
// Lazy load heavy components
const HeavyChart = lazy(() => import('./components/HeavyChart'));
const DataTable = lazy(() => import('./components/DataTable'));
const ReportGenerator = lazy(() =>
import('./components/ReportGenerator').then(module => ({
default: module.ReportGenerator
}))
);
// Preload on hover
const preloadChart = () => {
import('./components/HeavyChart');
};
// Route-based code splitting
const routes = {
dashboard: lazy(() => import('./pages/Dashboard')),
analytics: lazy(() => import('./pages/Analytics')),
settings: lazy(() => import('./pages/Settings')),
};
function App() {
const [activeTab, setActiveTab] = useState('overview');
const [isPending, startTransition] = useTransition();
const handleTabChange = (tab: string) => {
// Use transition for non-urgent updates
startTransition(() => {
setActiveTab(tab);
});
};
return (
<div className="app">
<nav>
<button
onClick={() => handleTabChange('overview')}
onMouseEnter={preloadChart}
>
Overview
</button>
<button onClick={() => handleTabChange('details')}>
Details
</button>
<button onClick={() => handleTabChange('reports')}>
Reports
</button>
</nav>
{isPending && <LoadingBar />}
<Suspense fallback={<FullPageSpinner />}>
{activeTab === 'overview' && <HeavyChart />}
{activeTab === 'details' && <DataTable />}
{activeTab === 'reports' && <ReportGenerator />}
</Suspense>
</div>
);
}
// Nested suspense for progressive loading
function Dashboard() {
return (
<div className="dashboard">
{/* Critical content loads first */}
<Suspense fallback={<HeaderSkeleton />}>
<DashboardHeader />
</Suspense>
<div className="dashboard-grid">
{/* Non-critical content can load independently */}
<Suspense fallback={<CardSkeleton />}>
<RevenueCard />
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<UsersCard />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<ActivityChart />
</Suspense>
</div>
</div>
);
}
Example 5: Avoiding Common Performance Pitfalls
import { memo, useMemo, useCallback, useState, useRef } from 'react';
// ❌ BAD: Inline object creates new reference every render
function BadComponent() {
return <Child style={{ color: 'red' }} />;
}
// ✅ GOOD: Stable reference
const childStyle = { color: 'red' };
function GoodComponent() {
return <Child style={childStyle} />;
}
// ❌ BAD: Inline function creates new reference
function BadHandler() {
return <Button onClick={() => console.log('clicked')} />;
}
// ✅ GOOD: useCallback for stable reference
function GoodHandler() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <Button onClick={handleClick} />;
}
// ❌ BAD: Spreading state causes all children to re-render
function BadContext() {
const [state, setState] = useState({ user: null, theme: 'dark', locale: 'en' });
return (
<AppContext.Provider value={{ ...state, setState }}>
<App />
</AppContext.Provider>
);
}
// ✅ GOOD: Split contexts by update frequency
function GoodContext() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('dark');
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<App />
</ThemeContext.Provider>
</UserContext.Provider>
);
}
// ❌ BAD: Computing in render without memoization
function BadComputation({ items }: { items: Item[] }) {
// Runs on every render
const sorted = items.slice().sort((a, b) => a.name.localeCompare(b.name));
return <List items={sorted} />;
}
// ✅ GOOD: Memoized computation
function GoodComputation({ items }: { items: Item[] }) {
const sorted = useMemo(
() => items.slice().sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
return <List items={sorted} />;
}
// Using refs for values that don't need re-renders
function FormWithRef() {
// ❌ Causes re-render on every keystroke
// const [value, setValue] = useState('');
// ✅ No re-renders, access via ref.current
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = () => {
const value = inputRef.current?.value;
submitForm(value);
};
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} />
<button type="submit">Submit</button>
</form>
);
}
Anti-Patterns
❌ Premature Optimization
// WRONG - memoizing everything without profiling
const Component = memo(({ title }) => <h1>{title}</h1>);
const memoizedTitle = useMemo(() => title.toUpperCase(), [title]);
// ✅ CORRECT - profile first, then optimize bottlenecks
function Component({ title }) {
return <h1>{title}</h1>;
}
❌ Incorrect Dependency Arrays
// WRONG - missing dependency causes stale closure
const handleClick = useCallback(() => {
console.log(count); // Stale!
}, []); // Missing count
// ✅ CORRECT
const handleClick = useCallback(() => {
console.log(count);
}, [count]);