# Performance
React performance patterns: memoization, code splitting, virtualization, React Compiler, profiling, and Web Vitals.
---
## Memoization
### React.memo
Skips re-render when props haven't changed (shallow equality by default).
```tsx
import { memo, useCallback, useState } from 'react';
interface ListItemProps {
item: { id: string; name: string; count: number };
onDelete: (id: string) => void;
}
// Memoize expensive list items so parent re-renders don't cascade
const ListItem = memo(function ListItem({ item, onDelete }: ListItemProps) {
console.log(`Rendering ${item.name}`); // only logs when item or onDelete changes
return (
{item.name} ({item.count})
);
});
// Custom comparison — return true to SKIP re-render
const ExpensiveChart = memo(
function ExpensiveChart({ data, config }: ChartProps) {
return ;
},
(prevProps, nextProps) => {
// Only re-render if data length changes or config changes
return (
prevProps.data.length === nextProps.data.length &&
prevProps.config.type === nextProps.config.type
);
}
);
// Parent must stabilize callbacks with useCallback to benefit from memo
function ItemList({ items }: { items: Item[] }) {
const [filter, setFilter] = useState('');
// Without useCallback, new function reference every render → memo is useless
const handleDelete = useCallback((id: string) => {
deleteItem(id);
}, []); // stable — no deps
return (
{items.map(item => (
))}
);
}
```
### When NOT to Use React.memo
```tsx
// BAD: memo on a component that almost always re-renders anyway
const SimpleDiv = memo(({ children }: { children: React.ReactNode }) => (
{children}
));
// BAD: memo where props contain new objects/arrays every render
function Parent() {
return (
// options is a new array every render — memo never skips
);
}
// GOOD: only memo when:
// 1. Component renders the same output given the same props
// 2. Re-renders frequently with same props (large lists, heavy computation)
// 3. Props are primitives or stable references
```
### useMemo
```tsx
import { useMemo, useState } from 'react';
function ProductList({ products }: { products: Product[] }) {
const [sortBy, setSortBy] = useState<'price' | 'name'>('name');
const [filter, setFilter] = useState('');
// Expensive: filter + sort on every render without memoization
const processedProducts = useMemo(() => {
const filtered = products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase())
);
return filtered.sort((a, b) =>
sortBy === 'price' ? a.price - b.price : a.name.localeCompare(b.name)
);
}, [products, filter, sortBy]); // only recalculates when these change
return (
{processedProducts.map(p => )}
);
}
// When NOT to use useMemo
function BadUsage() {
// BAD: simple operations don't need memoization — the overhead costs more
const doubled = useMemo(() => count * 2, [count]);
const greeting = useMemo(() => `Hello, ${name}`, [name]);
// GOOD: compute inline
const doubled = count * 2;
const greeting = `Hello, ${name}`;
}
```
### useCallback
```tsx
import { useCallback, useState, memo } from 'react';
// useCallback returns a stable function reference
// Only useful when passed to: memo() components, useEffect dep arrays, other callbacks
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
// Stable reference: won't cause SearchResults to re-render when SearchPage renders
const handleResultClick = useCallback((id: string) => {
trackClick(id); // does not depend on any state
}, []);
// Correct deps: includeArchived is used inside the callback
const [includeArchived, setIncludeArchived] = useState(false);
const search = useCallback(async (q: string) => {
const data = await fetchResults(q, { includeArchived });
setResults(data);
}, [includeArchived]); // re-created when includeArchived changes
return (
<>
>
);
}
```
---
## Code Splitting
### React.lazy + Suspense
```tsx
import { lazy, Suspense, useState } from 'react';
// Dynamic import — loaded only when rendered
const HeavyEditor = lazy(() => import('./HeavyEditor'));
const DataVizChart = lazy(() => import('./DataVizChart'));
// Preload on hover for instant perceived load
function preloadEditor() {
const promise = import('./HeavyEditor');
return promise;
}
function Dashboard() {
const [showEditor, setShowEditor] = useState(false);
return (
{showEditor && (
}>
)}
}>
);
}
```
### Route-Based Splitting (React Router)
```tsx
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
// Each route is its own chunk
const HomePage = lazy(() => import('./pages/Home'));
const DashboardPage = lazy(() => import('./pages/Dashboard'));
const SettingsPage = lazy(() => import('./pages/Settings'));
function App() {
return (
}>
} />
} />
} />
);
}
```
---
## Avoiding Re-renders
### State Colocation
```tsx
// BAD: state in parent causes all children to re-render
function Parent() {
const [inputValue, setInputValue] = useState('');
return (
<>
setInputValue(e.target.value)} />
{/* re-renders on every keystroke! */}
>
);
}
// GOOD: colocate state where it's needed
function InputSection() {
const [inputValue, setInputValue] = useState('');
return setInputValue(e.target.value)} />;
}
function Parent() {
return (
<>
{/* only this re-renders */}
{/* never re-renders */}
>
);
}
```
### Children Pattern
```tsx
// BAD: wrapping component re-renders on every parent render
function Wrapper() {
const [count, setCount] = useState(0);
return (
{/* re-renders even though it doesn't use count */}
);
}
// GOOD: pass slow component as children — it's created in parent, not re-rendered
function WrapperWithChildren({ children }: { children: React.ReactNode }) {
const [count, setCount] = useState(0);
return (
{children} {/* reference is stable, SlowComponent doesn't re-render */}
>
);
}
```
### useDeferredValue
```tsx
import { useState, useDeferredValue, memo } from 'react';
// useDeferredValue: defer a value derived from props/state
// Unlike useTransition, works when you don't own the state setter
function SearchResults({ query }: { query: string }) {
// Defer the slow part — input stays responsive
const deferredQuery = useDeferredValue(query);
return (
);
}
// Must be memoized for useDeferredValue to have effect
const SlowResultsList = memo(function SlowResultsList({ query }: { query: string }) {
// Expensive rendering — now deferred
const results = heavySearch(query);
return results.map(r => );
});
```
---
## Virtualization
For lists with more than 100 items, only render what's visible.
```tsx
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 60, // estimated row height in px
overscan: 5, // render 5 extra items outside viewport
});
return (
// Scrollable container — must have a fixed height
{/* Total height spacer so scrollbar is sized correctly */}
);
}
// LCP image — must be priority
function HeroImage() {
return (
);
}
```
---
## Performance Anti-patterns
| Anti-pattern | Problem | Fix |
|--------------|---------|-----|
| `memo` on everything | Comparison overhead, false optimization | Profile first; only memo when re-renders are measured problem |
| `useMemo` for cheap computations | Overhead of memoization > cost of computation | Only memoize if computation takes >1ms |
| `useCallback` without memoized consumers | Stable reference with no benefit | Only use when callback is dep in `useEffect` or passed to `memo` component |
| No `key` strategy for lists | React unmounts/remounts on reorder | Stable unique IDs from data |
| Inline object/array props on `memo` components | New reference every render defeats memo | `useMemo` the value or move outside component |
| Not virtualizing long lists | Renders thousands of DOM nodes | Use `@tanstack/react-virtual` for 100+ items |
| All JS in single bundle | Slow initial load | Route-based code splitting with `lazy` |
| `useEffect` polling instead of WebSocket/SSE | Constant network requests | Switch to real-time transport |
| Importing full lodash/moment | Huge bundle impact | Use tree-shakeable alternatives or native APIs |