React Performance: 8 Fixes That Actually Matter (2026)
I audited 15 React apps. These 8 optimizations appeared in every slow one.
1. Stop Re-rendering Everything
// ❌ BAD: Every keystroke re-renders the entire form
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
return (
<form>
<ExpensiveChart data={heavyData} /> {/* Re-renders on every keystroke! */}
<input value={name} onChange={e => setName(e.target.value)} />
<input value={email} onChange={e => setEmail(e.target.value)} />
</form>
);
}
// ✅ GOOD: Memoize expensive components
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
return (
<form>
<MemoizedChart data={heavyData} />
<input value={name} onChange={e => setName(e.target.value)} />
<input value={email} onChange={e => setEmail(e.target.value)} />
</form>
);
}
const MemoizedChart = React.memo(function Chart({ data }) {
return <ExpensiveChart data={data} />;
});
2. Use the Right State Management
// ❌ BAD: Context re-renders ALL consumers on ANY change
const ThemeContext = createContext();
const UserContext = createContext();
function App() {
const [theme, setTheme] = useState('dark');
const [user, setUser] = useState(null);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<UserContext.Provider value={{ user, setUser }}>
<Dashboard /> {/* Re-renders when theme OR user changes */}
</UserContext.Provider>
</ThemeContext.Provider>
);
}
// ✅ GOOD: Split contexts + use libraries for complex state
// Option 1: Separate contexts (simple apps)
<ThemeProvider>
<UserProvider>
<Dashboard />
</UserProvider>
</ThemeProvider>
// Option 2: Use Zustand (complex apps)
import { create } from 'zustand';
const useTheme = create((set) => ({
theme: 'dark',
setTheme: (t) => set({ theme: t }),
}));
// Components only re-render when THEIR slice changes
function ThemeToggle() {
const theme = useTheme(s => s.theme); // Only re-renders if theme changes
const setTheme = useTheme(s => s.setTheme);
return <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>Toggle</button>;
}
3. Lazy Load Routes
// ❌ BAD: All page components loaded upfront
import Home from './pages/Home';
import Dashboard from './pages/Dashboard';
import Settings from './pages/Settings';
// Bundle includes ALL pages even if user never visits them
// ✅ GOOD: Lazy load each route
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
Impact: Dashboard app bundle went from 1.2MB to 180KB initial load.
4. Virtualize Long Lists
// ❌ BAD: Renders ALL 10,000 items
function UserList({ users }) {
return (
<div>
{users.map(user => (
<UserRow key={user.id} user={user} />
))}
</div>
);
}
// ✅ GOOD: Only renders visible items
import { useVirtualizer } from '@tanstack/react-virtual';
function UserList({ users }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: users.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48, // Estimated row height
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map(virtualItem => (
<UserRow
key={virtualItem.key}
user={users[virtualItem.index]}
style={{
position: 'absolute',
top: 0,
transform: `translateY(${virtualItem.start}px)`
}}
/>
))}
</div>
</div>
);
}
Impact: 10,000 row table: render time dropped from 4.2s to 16ms.
5. Debounce Expensive Operations
// ❌ BAD: Search fires on every keystroke
function SearchBar() {
const [query, setQuery] = useState('');
const handleChange = (e) => {
setQuery(e.target.value);
searchAPI(e.target.value); // API call on EVERY keystroke!
};
return <input value={query} onChange={handleChange} />;
}
// ✅ GOOD: Debounce the API call
import { useDebouncedValue } from '@mantine/hooks';
function SearchBar() {
const [query, setQuery] = useState('');
const [debouncedQuery] = useDebouncedValue(query, 300);
useEffect(() => {
if (debouncedQuery) {
searchAPI(debouncedQuery);
}
}, [debouncedQuery]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
6. Optimize Images
// ❌ BAD: Full resolution images
<img src="/hero-banner.jpg" alt="Banner" />
// ✅ GOOD: Responsive images + lazy loading + WebP
<picture>
<source srcSet="/hero-banner.avif" type="image/avif" />
<source srcSet="/hero-banner.webp" type="image/webp" />
<img
src="/hero-banner.jpg"
alt="Banner"
loading="lazy"
decoding="async"
width={1200}
height={600}
/>
</picture>
Impact: Page weight dropped from 8MB to 1.2MB after converting to WebP + lazy loading.
7. Use useCallback and useMemo Wisely
// ✅ DO: Memoize callbacks passed to children
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // Stable reference — child won't re-render
return <MemoizedChild onClick={handleClick} count={count} />;
}
// ❌ DON'T: Memoize everything "just in case"
function BadComponent() {
const value = useMemo(() => 2 + 2, []); // Overkill — this is trivial
const fn = useCallback(() => 'hello', []); // Overkill — no child depends on it
return <div>{value}</div>;
}
// Rule: Only memoize if the value is expensive to compute
// OR if it's passed to a memoized child component
8. Measure Before Optimizing
// React DevTools Profiler — built-in!
// 1. Open React DevTools → Profiler tab
// 2. Click record
// 3. Interact with your app
// 4. Stop recording
// 5. See exactly which components re-rendered and why
// Chrome Performance tab for non-React performance:
// 1. F12 → Performance tab
// 2. Record
// 3. Interact
// 4. Stop
// 5. Check for long tasks (>50ms), layout thrashing, paint
// Web Vitals in code:
import { onCLS, onFID, onLCP, onINP } from 'web-vitals';
onCLS(console.log); // Cumulative Layout Shift (< 0.1)
onFID(console.log); // First Input Delay (< 100ms)
onLCP(console.log); // Largest Contentful Paint (< 2.5s)
onINP(console.log); // Interaction to Next Paint (< 200ms)
The Quick Wins Checklist
| Fix | Effort | Impact | Do It First? |
|---|---|---|---|
| Lazy load routes | Low | High | ✅ |
| Image optimization | Low | High | ✅ |
| Debounce inputs | Low | Medium | ✅ |
| Virtualize lists | Medium | High | For long lists |
| Split contexts | Medium | High | For complex state |
| React.memo | Low | Medium | Only where needed |
| useCallback/useMemo | Low | Low | Only when needed |
| State library | Medium | High | For complex apps |
The golden rule: Measure first, optimize second. Don't guess.
What's the biggest React performance fix you've made? Share your story!
Follow @armorbreak for more React tips.
Top comments (0)