Performance Optimization
React is fast by default, but as your application grows, you may encounter performance bottlenecks. This guide covers strategies for identifying and resolving common performance issues.
Production Builds
Always use production builds for deployed applications. Development builds include helpful warnings and debugging tools but are significantly slower.
# Create React App
npm run build
# Vite
npm run build
# Next.js
npm run build
Production builds enable optimizations like code minification, dead code elimination, and removal of development-only warnings. React’s production build is typically 2-3x faster than development mode.
Avoiding Unnecessary Renders
One of the most common performance issues is components re-rendering when their output hasn’t changed.
Before: Unnecessary Re-renders
function ParentComponent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<ExpensiveChild data="static" />
</div>
);
}
function ExpensiveChild({ data }) {
// This re-renders every time ParentComponent updates,
// even though 'data' never changes
const result = performExpensiveCalculation(data);
return <div>{result}</div>;
}
After: Optimized with React.memo
function ParentComponent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<ExpensiveChild data="static" />
</div>
);
}
// ExpensiveChild now only re-renders when props actually change
const ExpensiveChild = React.memo(function ExpensiveChild({ data }) {
const result = performExpensiveCalculation(data);
return <div>{result}</div>;
});
Use React DevTools Profiler to identify components that re-render frequently. Focus optimization efforts on components that render slowly or render very often.
The React DevTools Profiler helps you visualize component render performance.
Using the Profiler
- Install React DevTools
- Open the Profiler tab
- Click the record button
- Interact with your app
- Stop recording to analyze results
What to Look For
- Flame graphs: Visualize which components took the longest to render
- Ranked charts: See components sorted by render time
- Component charts: Track individual component render counts
- Yellow/orange bars: Indicate slower renders that may need optimization
import { Profiler } from 'react';
function onRenderCallback(
id, // "profile-name"
phase, // "mount" or "update"
actualDuration, // Time spent rendering
baseDuration, // Estimated time without memoization
startTime, // When React began rendering
commitTime // When React committed the update
) {
console.log(`${id} (${phase}) took ${actualDuration}ms`);
}
function App() {
return (
<Profiler id="app" onRender={onRenderCallback}>
<YourComponents />
</Profiler>
);
}
See the Profiler documentation for more details.
1. Creating New Objects/Functions in Render
Problem: Creating new object references on every render prevents memoization.
// ❌ Bad: New object created on every render
function UserProfile({ userId }) {
return (
<UserCard
user={{ id: userId, preferences: { theme: 'dark' } }}
onUpdate={() => console.log('updated')}
/>
);
}
Solution: Use useMemo and useCallback to maintain stable references.
// ✅ Good: Stable references
function UserProfile({ userId }) {
const user = useMemo(
() => ({ id: userId, preferences: { theme: 'dark' } }),
[userId]
);
const handleUpdate = useCallback(
() => console.log('updated'),
[]
);
return <UserCard user={user} onUpdate={handleUpdate} />;
}
See the Memoization documentation for more details.
2. Large Lists Without Keys
Problem: React can’t efficiently update lists without stable keys.
// ❌ Bad: Using index as key
{items.map((item, index) => (
<ListItem key={index} data={item} />
))}
Solution: Use unique, stable identifiers.
// ✅ Good: Using stable ID
{items.map(item => (
<ListItem key={item.id} data={item} />
))}
3. Expensive Calculations in Render
Problem: Heavy computations run on every render.
// ❌ Bad: Calculation runs every render
function DataTable({ data }) {
const sortedData = data.sort().filter(item => item.active);
return <Table data={sortedData} />;
}
Solution: Memoize expensive calculations.
// ✅ Good: Calculation only runs when data changes
function DataTable({ data }) {
const sortedData = useMemo(
() => data.sort().filter(item => item.active),
[data]
);
return <Table data={sortedData} />;
}
4. Context Updates Causing Wide Re-renders
Problem: Context updates cause all consumers to re-render.
// ❌ Bad: Single context with multiple values
const AppContext = createContext();
function Provider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
return (
<AppContext.Provider value={{ user, setUser, theme, setTheme }}>
{children}
</AppContext.Provider>
);
}
Solution: Split contexts by update frequency.
// ✅ Good: Separate contexts for different concerns
const UserContext = createContext();
const ThemeContext = createContext();
function Provider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
</UserContext.Provider>
);
}
PureComponent for Class Components
For class components, extend PureComponent instead of Component for automatic shallow prop comparison (see ReactBaseClasses.js:132).
import { PureComponent } from 'react';
// Automatically skips re-render if props haven't changed (shallow comparison)
class UserCard extends PureComponent {
render() {
return <div>{this.props.name}</div>;
}
}
PureComponent uses shallow comparison. If you pass objects or arrays as props, ensure they have stable references or the comparison will fail and cause unnecessary re-renders.
Code Splitting
Split large bundles using React.lazy() and dynamic imports.
import { lazy, Suspense } from 'react';
// Component is only loaded when rendered
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
);
}
Virtualization for Long Lists
For lists with hundreds or thousands of items, render only visible items.
import { FixedSizeList } from 'react-window';
function LargeList({ items }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={35}
width="100%"
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
);
}
Use libraries like react-window or react-virtualized for efficient list rendering. These libraries only render items in the viewport, dramatically improving performance for large datasets.
Use the Performance tab in Chrome DevTools:
- Open DevTools (F12)
- Go to Performance tab
- Click Record
- Interact with your app
- Stop recording and analyze the flame chart
User Timing API
function ProfiledComponent() {
useEffect(() => {
performance.mark('component-start');
return () => {
performance.mark('component-end');
performance.measure(
'component-duration',
'component-start',
'component-end'
);
};
}, []);
return <div>Content</div>;
}
Optimization Checklist
Don’t optimize prematurely! Always measure first. Premature optimization can make code harder to maintain without meaningful performance gains. Focus on optimizing slow components identified through profiling.