When to Optimize
Performance optimization without measurement is premature. Start by profiling your app to identify actual bottlenecks. Not every component needs optimization.
Optimize when:
- Users report lag or stuttering
- Profiler shows slow renders (>16ms)
- Large lists or tables feel sluggish
- Complex forms have input delay
Don’t optimize:
- Before you have performance data
- Components that render once
- Simple presentational components
Profiling: Find the Real Problems
React DevTools Profiler
# Install React DevTools
# Chrome: https://chrome.google.com/webstore (search React Developer Tools)
How to profile:
- Open React DevTools → Profiler tab
- Click record
- Interact with your app
- Stop recording
- Analyze flame graph
What to look for:
- Components with long render times (red/yellow)
- Components that render unnecessarily
- Render cascades (one change triggers many renders)
Browser Performance Tab
// Add performance marks
performance.mark('render-start');
// ... render logic
performance.mark('render-end');
performance.measure('render', 'render-start', 'render-end');
View in DevTools → Performance tab → User Timing section.
Preventing Unnecessary Renders
React.memo: Component Memoization
// ❌ Re-renders even when props haven't changed
function UserCard({ user }) {
console.log('UserCard rendered');
return <div>{user.name}</div>;
}
// ✅ Only re-renders when user changes
const UserCard = React.memo(function UserCard({ user }) {
console.log('UserCard rendered');
return <div>{user.name}</div>;
});
// ✅ With custom comparison
const UserCard = React.memo(
function UserCard({ user }) {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => {
// Return true to skip render
return prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name;
}
);
When to use:
- Expensive render logic
- Pure presentational components
- Components receiving complex props
When NOT to use:
- Props change frequently
- Component is already fast
- Parent always re-renders anyway
useMemo: Memoize Expensive Calculations
function ProductList({ products, filters }) {
// ❌ Filters on every render
const filteredProducts = products.filter(
p => p.category === filters.category
);
// ✅ Only recalculates when dependencies change
const filteredProducts = useMemo(
() => products.filter(p => p.category === filters.category),
[products, filters.category]
);
return (
<ul>
{filteredProducts.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
Real-world example:
function DataTable({ data }) {
const sortedData = useMemo(() => {
console.log('Sorting data...');
return [...data].sort((a, b) =>
a.date.localeCompare(b.date)
);
}, [data]);
const stats = useMemo(() => {
console.log('Calculating stats...');
return {
total: sortedData.length,
sum: sortedData.reduce((acc, item) => acc + item.value, 0),
avg: sortedData.reduce((acc, item) => acc + item.value, 0) / sortedData.length,
};
}, [sortedData]);
return (
<div>
<p>Total: {stats.total}, Avg: {stats.avg.toFixed(2)}</p>
<ul>
{sortedData.map(item => (
<li key={item.id}>{item.name}: {item.value}</li>
))}
</ul>
</div>
);
}
useCallback: Stabilize Function References
function TodoList({ todos }) {
// ❌ New function on every render → child re-renders
const handleToggle = (id) => {
// toggle logic
};
// ✅ Stable function reference
const handleToggle = useCallback((id) => {
// toggle logic
}, []);
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
/>
))}
</ul>
);
}
const TodoItem = React.memo(function TodoItem({ todo, onToggle }) {
return (
<li onClick={() => onToggle(todo.id)}>
{todo.text}
</li>
);
});
When useCallback matters:
// ✅ Matters: Passed to memoized child
const Child = React.memo(({ onClick }) => { /* ... */ });
const Parent = () => {
const handleClick = useCallback(() => {}, []);
return <Child onClick={handleClick} />;
};
// ❌ Doesn't matter: Child not memoized
const Child = ({ onClick }) => { /* ... */ };
const Parent = () => {
const handleClick = useCallback(() => {}, []); // Unnecessary
return <Child onClick={handleClick} />;
};
Code Splitting: Load Less, Faster
Route-Based Splitting
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// ❌ All routes loaded upfront
import Home from './pages/Home';
import Dashboard from './pages/Dashboard';
import Settings from './pages/Settings';
// ✅ Routes loaded on demand
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
Component-Based Splitting
// Heavy component loaded on interaction
const HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>
Show Chart
</button>
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart data={chartData} />
</Suspense>
)}
</div>
);
}
Preloading on Hover
const Settings = lazy(() => import('./Settings'));
function Nav() {
const handleMouseEnter = () => {
// Preload Settings before click
import('./Settings');
};
return (
<nav>
<Link
to="/settings"
onMouseEnter={handleMouseEnter}
>
Settings
</Link>
</nav>
);
}
Virtualization: Handle Large Lists
Why Virtualization Matters
// ❌ Renders 10,000 DOM nodes
function BadList({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
// Rendering 10,000 items:
// - Initial render: 2-3 seconds
// - Scroll: Janky, dropped frames
// - Memory: 50-100MB
React Window
npm install react-window
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
// Rendering 10,000 items:
// - Initial render: 50ms
// - Scroll: Smooth 60fps
// - Memory: 2-5MB
Variable Size Items
import { VariableSizeList } from 'react-window';
function VariableList({ items }) {
const getItemSize = (index) => {
// Dynamic height based on content
return items[index].expanded ? 200 : 50;
};
const Row = ({ index, style }) => (
<div style={style}>
{/* Item content */}
</div>
);
return (
<VariableSizeList
height={600}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
>
{Row}
</VariableSizeList>
);
}
State Management Performance
Context API Optimization
// ❌ Single context causes unnecessary renders
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [settings, setSettings] = useState({});
// Every state change re-renders all consumers
return (
<AppContext.Provider value={{ user, theme, settings, setUser, setTheme, setSettings }}>
{children}
</AppContext.Provider>
);
}
// ✅ Split contexts by update frequency
const UserContext = createContext();
const ThemeContext = createContext();
const SettingsContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [settings, setSettings] = useState({});
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<SettingsContext.Provider value={{ settings, setSettings }}>
{children}
</SettingsContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}
Memoize Context Values
function UserProvider({ children }) {
const [user, setUser] = useState(null);
// ❌ New object on every render
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
// ✅ Stable reference
const value = useMemo(
() => ({ user, setUser }),
[user]
);
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}
Zustand: Better Performance Out of the Box
npm install zustand
import create from 'zustand';
// Only selected state triggers re-renders
const useStore = create((set) => ({
user: null,
theme: 'light',
setUser: (user) => set({ user }),
setTheme: (theme) => set({ theme }),
}));
// ✅ Only re-renders when user changes
function UserProfile() {
const user = useStore((state) => state.user);
return <div>{user?.name}</div>;
}
// ✅ Only re-renders when theme changes
function ThemeToggle() {
const theme = useStore((state) => state.theme);
const setTheme = useStore((state) => state.setTheme);
return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle</button>;
}
Debouncing and Throttling
Debounce Search Input
import { useState, useEffect } from 'react';
function SearchInput() {
const [query, setQuery] = useState('');
const [debouncedQuery, setDebouncedQuery] = useState('');
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedQuery(query);
}, 300);
return () => clearTimeout(timer);
}, [query]);
useEffect(() => {
if (debouncedQuery) {
// Expensive search operation
searchAPI(debouncedQuery);
}
}, [debouncedQuery]);
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
Throttle Scroll Events
import { useEffect, useRef } from 'react';
function InfiniteScroll({ onLoadMore }) {
const lastCall = useRef(0);
useEffect(() => {
const handleScroll = () => {
const now = Date.now();
// Throttle to max once per 200ms
if (now - lastCall.current < 200) return;
lastCall.current = now;
const scrollHeight = document.documentElement.scrollHeight;
const scrollTop = document.documentElement.scrollTop;
const clientHeight = document.documentElement.clientHeight;
if (scrollTop + clientHeight >= scrollHeight - 100) {
onLoadMore();
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [onLoadMore]);
return <div>{/* content */}</div>;
}
Image Optimization
Lazy Loading Images
function ImageGallery({ images }) {
return (
<div>
{images.map(img => (
<img
key={img.id}
src={img.url}
alt={img.alt}
loading="lazy"
decoding="async"
/>
))}
</div>
);
}
Responsive Images
function ResponsiveImage({ src, alt }) {
return (
<picture>
<source
srcSet={`${src}?w=400 400w, ${src}?w=800 800w, ${src}?w=1200 1200w`}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
<img src={`${src}?w=800`} alt={alt} loading="lazy" />
</picture>
);
}
Bundle Size Optimization
Analyze Bundle
# Create React App
npm run build
npx webpack-bundle-analyzer build/static/js/*.js
# Vite
npm run build
npx vite-bundle-visualizer
Tree Shaking
// ❌ Imports entire library
import _ from 'lodash';
const result = _.debounce(fn, 300);
// ✅ Imports only what you need
import debounce from 'lodash/debounce';
const result = debounce(fn, 300);
// ✅ Even better: Use modern alternatives
import { debounce } from 'lodash-es'; // ES modules version
Dynamic Imports
// Load heavy library only when needed
function ChartComponent({ data }) {
const [Chart, setChart] = useState(null);
useEffect(() => {
import('chart.js').then(module => {
setChart(() => module.Chart);
});
}, []);
if (!Chart) return <Skeleton />;
return <Chart data={data} />;
}
Production Optimizations
Enable Production Build
// Automatic in production builds, but verify:
if (process.env.NODE_ENV === 'production') {
// Production-only code
console.log = () => {};
console.warn = () => {};
}
Compression
// vite.config.js
import compression from 'vite-plugin-compression';
export default {
plugins: [
compression({
algorithm: 'brotliCompress',
ext: '.br',
}),
],
};
Common Anti-Patterns
1. Premature Optimization
// ❌ Unnecessary memoization
const SimpleComponent = React.memo(function ({ text }) {
return <p>{text}</p>;
});
// ✅ Just a regular component
function SimpleComponent({ text }) {
return <p>{text}</p>;
}
2. Incorrect Dependencies
// ❌ Missing dependency
const memoizedValue = useMemo(
() => expensiveCalculation(a, b),
[a] // Missing b!
);
// ✅ All dependencies listed
const memoizedValue = useMemo(
() => expensiveCalculation(a, b),
[a, b]
);
3. Over-Memoization
// ❌ Memoizing everything
function Component() {
const value1 = useMemo(() => prop1 + prop2, [prop1, prop2]);
const value2 = useMemo(() => prop3 * 2, [prop3]);
const handler = useCallback(() => {}, []);
// More memoization overhead than render cost
}
// ✅ Memoize only expensive operations
function Component() {
const value1 = prop1 + prop2; // Cheap, no memo needed
const value2 = prop3 * 2; // Cheap, no memo needed
const expensiveValue = useMemo(
() => reallyExpensiveCalculation(data),
[data]
);
}
Performance Checklist
- Profile before optimizing
- Enable React.StrictMode in development
- Use React DevTools Profiler
- Implement code splitting for routes
- Virtualize long lists
- Memoize expensive calculations
- Optimize images (lazy load, responsive)
- Split Context to prevent unnecessary renders
- Debounce user input
- Use production build for deployment
- Analyze bundle size
- Enable compression (gzip/brotli)
Measuring Success
Before and After Metrics
- Initial Load: Time to interactive
- Runtime: 60fps during scrolling
- Memory: No memory leaks
- Bundle Size: < 200KB gzipped for initial load
Tools
- Chrome DevTools Performance
- React DevTools Profiler
- Lighthouse
- Web Vitals
- webpack-bundle-analyzer
