| name | react-performance |
| description | React performance optimization patterns to prevent unnecessary re-renders, reduce bundle size, and improve user experience. Use when optimizing components, fixing slow renders, or reducing app size. Triggers on performance, optimize, slow, re-render, bundle, lazy, memo, useMemo, useCallback. |
React Performance Optimization
Performance patterns to prevent unnecessary re-renders, reduce bundle size, and create responsive UIs.
When to Use This Skill
- Optimizing slow components
- Reducing bundle size
- Fixing unnecessary re-renders
- Improving initial load time
- Debugging performance issues
Critical: Prevent Waterfalls
Impact: CRITICAL - The highest impact optimization.
What is a Waterfall?
Sequential requests where each waits for the previous:
Timeline:
[----Parent Data Fetch----]
[----Child Data Fetch----]
[----Grandchild Fetch----]
Total: 900ms
Anti-Pattern: Fetch on Mount
function ParentComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetchParentData().then(setData);
}, []);
if (!data) return <Loading />;
return <ChildComponent parentId={data.id} />;
}
function ChildComponent({ parentId }: { parentId: string }) {
const [childData, setChildData] = useState(null);
useEffect(() => {
fetchChildData(parentId).then(setChildData);
}, [parentId]);
return <div>{childData?.name}</div>;
}
Solution: Parallel Data Fetching
function ParentComponent() {
const { parentData, childData, isLoading } = useParallelData();
if (isLoading) return <Loading />;
return (
<div>
<ParentView data={parentData} />
<ChildView data={childData} />
</div>
);
}
function useParallelData() {
const [data, setData] = useState({ parentData: null, childData: null });
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
Promise.all([fetchParentData(), fetchChildData()])
.then(([parentData, childData]) => {
setData({ parentData, childData });
setIsLoading(false);
});
}, []);
return { ...data, isLoading };
}
With React Query/TanStack Query
function Dashboard() {
const { data: user } = useQuery(['user'], fetchUser);
const { data: tasks } = useQuery(['tasks'], fetchTasks);
const { data: sessions } = useQuery(['sessions'], fetchSessions);
return <DashboardView user={user} tasks={tasks} sessions={sessions} />;
}
Critical: Bundle Size Optimization
Impact: CRITICAL - Directly affects load time.
Avoid Barrel Import Anti-Pattern
import { Button, Input, Select, Modal, ... } from '@/components';
import { Button } from '@/components/button';
import { Input } from '@/components/input';
Lazy Loading Components
import { lazy, Suspense } from 'react';
import { SettingsPanel } from './SettingsPanel';
const SettingsPanel = lazy(() => import('./SettingsPanel'));
function App() {
const [showSettings, setShowSettings] = useState(false);
return (
<div>
<button onClick={() => setShowSettings(true)}>Settings</button>
<Suspense fallback={<Loading />}>
{showSettings && <SettingsPanel />}
</Suspense>
</div>
);
}
Route-Based Code Splitting
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Sessions = lazy(() => import('./pages/Sessions'));
const Settings = lazy(() => import('./pages/Settings'));
function AppRoutes() {
return (
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/sessions" element={<Sessions />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
Dynamic Imports for Heavy Libraries
import { marked } from 'marked';
import hljs from 'highlight.js';
async function renderMarkdown(content: string) {
const [{ marked }, hljs] = await Promise.all([
import('marked'),
import('highlight.js'),
]);
return marked(content, {
highlight: (code, lang) => hljs.highlight(code, { language: lang }).value,
});
}
High: Prevent Unnecessary Re-renders
Impact: HIGH - Common cause of sluggish UIs.
Identify Re-renders
useEffect(() => {
console.log('Component re-rendered');
});
Stable References with useCallback
function TaskList({ tasks }: { tasks: Task[] }) {
const handleComplete = (id: string) => {
completeTask(id);
};
return tasks.map(task => (
<TaskItem key={task.id} task={task} onComplete={handleComplete} />
));
}
function TaskList({ tasks }: { tasks: Task[] }) {
const handleComplete = useCallback((id: string) => {
completeTask(id);
}, []);
return tasks.map(task => (
<TaskItem key={task.id} task={task} onComplete={handleComplete} />
));
}
Memoize Expensive Computations
function TaskStats({ tasks }: { tasks: Task[] }) {
const stats = {
total: tasks.length,
completed: tasks.filter(t => t.status === 'completed').length,
pending: tasks.filter(t => t.status === 'pending').length,
avgPriority: tasks.reduce((sum, t) => sum + t.priority, 0) / tasks.length,
};
return <StatsDisplay stats={stats} />;
}
function TaskStats({ tasks }: { tasks: Task[] }) {
const stats = useMemo(() => ({
total: tasks.length,
completed: tasks.filter(t => t.status === 'completed').length,
pending: tasks.filter(t => t.status === 'pending').length,
avgPriority: tasks.reduce((sum, t) => sum + t.priority, 0) / tasks.length,
}), [tasks]);
return <StatsDisplay stats={stats} />;
}
Memoize Components
function TaskItem({ task, onComplete }: TaskItemProps) {
return (
<div className="task-item">
<span>{task.title}</span>
<button onClick={() => onComplete(task.id)}>Complete</button>
</div>
);
}
const TaskItem = memo(function TaskItem({ task, onComplete }: TaskItemProps) {
return (
<div className="task-item">
<span>{task.title}</span>
<button onClick={() => onComplete(task.id)}>Complete</button>
</div>
);
});
Avoid Object/Array Literals in JSX
function Component() {
return (
<Child
style={{ padding: 10, margin: 5 }} // New object each render!
items={['a', 'b', 'c']} // New array each render!
/>
);
}
const containerStyle = { padding: 10, margin: 5 };
const defaultItems = ['a', 'b', 'c'];
function Component() {
return (
<Child
style={containerStyle}
items={defaultItems}
/>
);
}
function Component({ padding }: { padding: number }) {
const style = useMemo(() => ({ padding, margin: 5 }), [padding]);
return <Child style={style} />;
}
Medium: Efficient State Management
Avoid State Lifting Anti-Pattern
function App() {
const [searchTerm, setSearchTerm] = useState('');
return (
<div>
<Header /> {/* Re-renders on every keystroke! */}
<SearchInput value={searchTerm} onChange={setSearchTerm} />
<SearchResults term={searchTerm} />
<Footer /> {/* Re-renders on every keystroke! */}
</div>
);
}
function SearchSection() {
const [searchTerm, setSearchTerm] = useState('');
return (
<div>
<SearchInput value={searchTerm} onChange={setSearchTerm} />
<SearchResults term={searchTerm} />
</div>
);
}
function App() {
return (
<div>
<Header /> {/* Won't re-render */}
<SearchSection />
<Footer /> {/* Won't re-render */}
</div>
);
}
Use Jotai for Atomic State
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
const tasksAtom = atom<Task[]>([]);
const selectedTaskIdAtom = atom<string | null>(null);
const selectedTaskAtom = atom((get) => {
const tasks = get(tasksAtom);
const selectedId = get(selectedTaskIdAtom);
return tasks.find(t => t.id === selectedId);
});
function TaskList() {
const tasks = useAtomValue(tasksAtom);
return tasks.map(task => <TaskItem key={task.id} task={task} />);
}
function TaskDetails() {
const task = useAtomValue(selectedTaskAtom);
return task ? <TaskView task={task} /> : null;
}
Medium: Virtualized Lists
For long lists (100+ items), render only visible items:
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualizedTaskList({ tasks }: { tasks: Task[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: tasks.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
<div ref={parentRef} className="h-[400px] overflow-auto">
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: 0,
transform: `translateY(${virtualRow.start}px)`,
height: `${virtualRow.size}px`,
}}
>
<TaskItem task={tasks[virtualRow.index]} />
</div>
))}
</div>
</div>
);
}
Performance Debugging
React DevTools Profiler
- Open React DevTools > Profiler tab
- Click Record
- Perform the slow action
- Click Stop
- Analyze:
- Which components re-rendered?
- How long did renders take?
- What triggered the render?
Common Performance Issues
| Symptom | Likely Cause | Solution |
|---|
| Entire app re-renders | State too high | Colocate state, use context selectors |
| List sluggish | Too many DOM nodes | Virtualize list |
| Slow initial load | Large bundle | Code split, lazy load |
| Input lag | Expensive computation in render | useMemo, debounce |
| Scroll jank | Layout thrashing | Use transform, virtual scroll |
Performance Checklist
Before Optimizing
Common Optimizations