React Performance Optimization
Profiling First
Always profile before optimizing. Use React DevTools Profiler to find slow renders.
// Wrap with Profiler to measure
import { Profiler } from 'react';
<Profiler
id="UserList"
onRender={(id, phase, actualDuration) => {
if (actualDuration > 16) { // slower than 60fps
console.warn(`${id} took ${actualDuration}ms in ${phase}`);
}
}}
>
<UserList />
</Profiler>
React.memo
Prevents re-renders when props haven't changed.
// Without memo: re-renders whenever parent renders
function UserCard({ user }: { user: User }) {
return <div>{user.name}</div>;
}
// With memo: only re-renders when user prop changes
const UserCard = React.memo(({ user }: { user: User }) => {
return <div>{user.name}</div>;
});
// Custom comparison
const UserCard = React.memo(
({ user }: { user: User }) => <div>{user.name}</div>,
(prev, next) => prev.user.id === next.user.id && prev.user.name === next.user.name
);
useMemo - Memoize Expensive Computations
function UserList({ users, searchQuery }: Props) {
// Without useMemo: filters on every render
// const filtered = users.filter(u => u.name.includes(searchQuery));
// With useMemo: only recomputes when users or searchQuery changes
const filtered = useMemo(
() => users.filter(u => u.name.toLowerCase().includes(searchQuery.toLowerCase())),
[users, searchQuery]
);
const stats = useMemo(() => ({
total: users.length,
active: users.filter(u => u.active).length,
admins: users.filter(u => u.role === 'admin').length,
}), [users]);
return (
<div>
<Stats {...stats} />
{filtered.map(user => <UserCard key={user.id} user={user} />)}
</div>
);
}
useCallback - Stable Function References
function UserList() {
const [users, setUsers] = useState<User[]>([]);
// Without useCallback: new function reference on every render
// -> causes UserCard to re-render even when users haven't changed
// const handleDelete = (id: string) => setUsers(u => u.filter(user => user.id !== id));
// With useCallback: stable reference, UserCard doesn't re-render unnecessarily
const handleDelete = useCallback((id: string) => {
setUsers(prev => prev.filter(user => user.id !== id));
}, []); // empty deps: function never changes
return (
<>
{users.map(user => (
<UserCard key={user.id} user={user} onDelete={handleDelete} />
))}
</>
);
}
Code Splitting with React.lazy
import { lazy, Suspense } from 'react';
// Load components only when needed
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
Virtualization for Long Lists
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
function VirtualUserList({ users }: { users: User[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: users.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 72, // estimated row height
overscan: 5,
});
return (
<div ref={parentRef} style={{ height: '600px', 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)`,
}}
>
<UserCard user={users[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}
useTransition for Non-Urgent Updates
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<User[]>([]);
const [isPending, startTransition] = useTransition();
function handleSearch(value: string) {
setQuery(value); // Urgent: update input immediately
startTransition(() => {
// Non-urgent: can defer if user is still typing
setResults(filterUsers(value));
});
}
return (
<div>
<input value={query} onChange={e => handleSearch(e.target.value)} />
{isPending ? <Spinner /> : <UserList users={results} />}
</div>
);
}
Premature optimization is the root of all evil. Profile first, then optimize the right things.