React Optimization
Optimize React rendering for better performance and lower INP
React's rendering behavior can significantly impact performance. Understanding when and why components re-render is key to optimization.
Understanding Re-renders
A component re-renders when:
- Its state changes
- Its parent re-renders
- Its context value changes
- Its props change (for non-memoized components)
// Parent re-renders → All children re-render
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<ExpensiveChild /> {/* Re-renders even though props unchanged */}
</div>
);
}React.memo
Prevent re-renders when props haven't changed:
// ❌ Re-renders on every parent render
function ExpensiveList({ items }) {
return items.map(item => <ListItem key={item.id} item={item} />);
}
// ✅ Only re-renders when items change
const ExpensiveList = memo(function ExpensiveList({ items }) {
return items.map(item => <ListItem key={item.id} item={item} />);
});Custom Comparison
const UserCard = memo(
function UserCard({ user, onSelect }) {
return (
<div onClick={() => onSelect(user.id)}>
{user.name}
</div>
);
},
(prevProps, nextProps) => {
// Only re-render if user ID changes
return prevProps.user.id === nextProps.user.id;
}
);useMemo
Cache expensive computations:
function ProductList({ products, category }) {
// ❌ Filters on every render
const filtered = products.filter(p => p.category === category);
// ✅ Only filters when dependencies change
const filtered = useMemo(
() => products.filter(p => p.category === category),
[products, category]
);
return <List items={filtered} />;
}When to Use useMemo
// ✅ Good: Expensive computation
const sortedData = useMemo(
() => data.sort((a, b) => complexSort(a, b)),
[data]
);
// ✅ Good: Referential equality for memo'd children
const chartConfig = useMemo(
() => ({ type: 'bar', data: processData(rawData) }),
[rawData]
);
// ❌ Unnecessary: Simple computation
const doubled = useMemo(() => count * 2, [count]);useCallback
Stabilize function references:
function SearchForm({ onSearch }) {
const [query, setQuery] = useState('');
// ❌ New function every render
const handleSubmit = () => onSearch(query);
// ✅ Stable function reference
const handleSubmit = useCallback(() => {
onSearch(query);
}, [query, onSearch]);
return (
<form onSubmit={handleSubmit}>
<input value={query} onChange={e => setQuery(e.target.value)} />
<MemoizedButton onClick={handleSubmit}>Search</MemoizedButton>
</form>
);
}React Compiler (React 19+)
module.exports = {
experimental: {
reactCompiler: true,
},
};Now memo, useMemo, useCallback are automatic!
function ProductList({ products, category }) {
// Compiler automatically memoizes
const filtered = products.filter(p => p.category === category);
return <List items={filtered} />;
}State Management Optimization
Colocate State
// ❌ State too high, causes unnecessary re-renders
function App() {
const [searchQuery, setSearchQuery] = useState('');
return (
<div>
<Header /> {/* Re-renders on every keystroke */}
<Search query={searchQuery} setQuery={setSearchQuery} />
<Content /> {/* Re-renders on every keystroke */}
</div>
);
}
// ✅ State colocated with component that uses it
function App() {
return (
<div>
<Header /> {/* Doesn't re-render */}
<Search /> {/* Owns its own state */}
<Content /> {/* Doesn't re-render */}
</div>
);
}Split Context
// ❌ Single context causes all consumers to re-render
const AppContext = createContext({ user: null, theme: 'dark', cart: [] });
// ✅ Split contexts by update frequency
const UserContext = createContext(null);
const ThemeContext = createContext('dark');
const CartContext = createContext([]);
// Components only subscribe to what they need
function CartIcon() {
const cart = useContext(CartContext); // Only re-renders on cart changes
return <span>{cart.length}</span>;
}State Selectors with Zustand/Redux
import { useStore } from '@/store';
// ❌ Re-renders on any store change
function UserName() {
const store = useStore();
return <span>{store.user.name}</span>;
}
// ✅ Only re-renders when name changes
function UserName() {
const name = useStore(state => state.user.name);
return <span>{name}</span>;
}List Optimization
Virtualization
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
overscan: 5,
});
return (
<div ref={parentRef} className="h-[400px] overflow-auto">
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: virtualRow.start,
height: virtualRow.size,
}}
>
<ListItem item={items[virtualRow.index]} />
</div>
))}
</div>
</div>
);
}Stable Keys
// ❌ Bad: Index as key causes full re-mount on reorder
{items.map((item, index) => (
<Item key={index} data={item} />
))}
// ✅ Good: Stable unique ID
{items.map(item => (
<Item key={item.id} data={item} />
))}React Transitions
import { useTransition, useState } from 'react';
function FilterableList({ items }) {
const [filter, setFilter] = useState('');
const [filteredItems, setFilteredItems] = useState(items);
const [isPending, startTransition] = useTransition();
function handleFilter(value: string) {
setFilter(value); // Urgent: update input immediately
startTransition(() => {
// Non-urgent: can be interrupted
setFilteredItems(items.filter(item =>
item.name.toLowerCase().includes(value.toLowerCase())
));
});
}
return (
<div>
<input
value={filter}
onChange={e => handleFilter(e.target.value)}
/>
<div style={{ opacity: isPending ? 0.7 : 1 }}>
{filteredItems.map(item => <Item key={item.id} item={item} />)}
</div>
</div>
);
}useDeferredValue
import { useDeferredValue, useMemo } from 'react';
function SearchResults({ query }) {
// Deferred value lags behind during heavy renders
const deferredQuery = useDeferredValue(query);
const results = useMemo(
() => expensiveSearch(deferredQuery),
[deferredQuery]
);
const isStale = query !== deferredQuery;
return (
<div style={{ opacity: isStale ? 0.7 : 1 }}>
{results.map(result => <Result key={result.id} {...result} />)}
</div>
);
}Profiling React Performance
React DevTools Profiler
Programmatic Profiling
import { Profiler } from 'react';
function onRenderCallback(
id: string,
phase: 'mount' | 'update',
actualDuration: number,
baseDuration: number,
) {
console.log(`${id} ${phase}: ${actualDuration}ms`);
}
<Profiler id="MyComponent" onRender={onRenderCallback}>
<MyComponent />
</Profiler>Checklist
Memoization
Use memo() for expensive pure components Prevent re-renders when props haven't changed.
Use useMemo for expensive computations Cache results of filtering, sorting, or transformations.
Use useCallback for stable function refs Prevent memoized children from re-rendering.
Consider React Compiler (React 19+) Automatic memoization eliminates manual optimization.
State
Colocate state near where it's used Move state down to prevent unnecessary parent re-renders.
Split context by update frequency Separate fast-changing from slow-changing state.
Use selectors with state libraries Subscribe only to specific slices of state.
Avoid unnecessary state Derive values from existing state when possible.
Lists
Virtualize long lists Use @tanstack/react-virtual for large datasets.
Use stable keys Use unique IDs, never array indices for dynamic lists.
Memoize list items Wrap list item components with memo().
Avoid inline object props Create objects outside render or memoize them.
Concurrent Features
Use useTransition for non-urgent updates Keep UI responsive during expensive state updates.
Use useDeferredValue for expensive renders Defer heavy child renders while showing stale data.
Implement Suspense boundaries Stream content progressively for better UX.
Consider streaming SSR Send initial HTML while data loads.