React's virtual DOM is fast, but that doesn't mean React applications are fast by default. As your application grows, performance bottlenecks emerge. The good news? Most can be eliminated with strategic optimization.
Understanding React's Render Cycle
Before optimizing, understand when React re-renders:
- State changes (useState, useReducer)
- Props changes from parent
- Parent component re-renders
- Context value changes
// Bad: New object reference every render
function Parent() {
const [count, setCount] = useState(0);
const config = { threshold: 10 }; // New object every render!
return <Child config={config} />; // Child re-renders unnecessarily
}
// Good: Stable reference
function Parent() {
const [count, setCount] = useState(0);
const config = useMemo(() => ({
threshold: 10
}), []); // Only created once
return <Child config={config} />;
}
Code Splitting & Lazy Loading
Don't ship code the user doesn't immediately need. React.lazy and Suspense make this trivial:
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
const Dashboard = lazy(() => import('./Dashboard'));
const Analytics = lazy(() => import('./Analytics'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
);
}
Use webpack-bundle-analyzer to visualize your bundle. Look for large dependencies that could be loaded on demand.
Virtualization for Large Lists
Rendering 1000 list items? Only render what's visible. react-window keeps DOM nodes minimal:
import { FixedSizeList as List } from 'react-window';
function VirtualList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<List
height={500}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</List>
);
}
// Renders only ~10 DOM nodes instead of 1000!
Don't virtualize small lists (<50 items). The overhead outweighs benefits. Measure first, optimize second.
State Management Optimization
Where you place state matters. Keep state as close to where it's used as possible:
- Local state: useState for component-only data
- Lifted state: Common parent for shared sibling data
- Context: Truly global, rarely changing data (theme, auth)
- External: Redux/Zustand for complex interdependent state
Avoid prop drilling by using composition patterns before reaching for context or Redux.