7 Common React Performance Mistakes (And How to Fix Them)
These 7 React performance mistakes silently slow down your app. Before-and-after code fixes, React DevTools tips, and a profiling guide to find the real culprits.
Get more content like this on Telegram!
Daily AI tips, notes & resources — free
I've reviewed a lot of React codebases, and the performance problems I find are usually not exotic. They're the same seven mistakes, over and over, made by developers who were focused on making things work rather than making them fast.
Most performance issues in React come from unnecessary re-renders. Your UI re-renders when state or props change — that's expected. But it also re-renders in a lot of situations where it shouldn't, and that's where the problems live.
Let me show you the mistakes and their fixes.
Setting Up: React DevTools Profiler
Before we get to the mistakes, a quick word on finding them. React DevTools has a Profiler tab that I consider essential.
How to use it:
- Install React DevTools browser extension
- Open DevTools → React tab → Profiler
- Click the record button (circle icon)
- Interact with the slow part of your app
- Click stop
- Read the flame graph
The flame graph shows you every component that rendered during your interaction. The width represents render time. Tall, wide bars are your targets.
Two key views:
- Flame chart: shows the call tree with render duration
- Ranked chart: sorts components by render time, most expensive at top
Look at "self time" (time spent in the component itself, not its children) to find actual bottlenecks rather than cascade effects.
Mistake 1: Creating New Object/Array References in JSX
This is the most common and sneakiest mistake. Every render creates new object literals, and when those get passed as props, they cause child re-renders even when the data hasn't changed.
// BAD — new object created on every render
function UserProfile({ userId }) {
return (
<Avatar
style={{ margin: '16px', borderRadius: '50%' }} // new object every time
config={{ size: 'large', variant: 'circle' }} // new object every time
/>
);
}
// GOOD — move constants outside the component
const avatarStyle = { margin: '16px', borderRadius: '50%' };
const avatarConfig = { size: 'large', variant: 'circle' };
function UserProfile({ userId }) {
return (
<Avatar
style={avatarStyle}
config={avatarConfig}
/>
);
}
The same issue applies to inline arrays and inline function definitions:
// BAD — new array every render
<Select options={['Option 1', 'Option 2', 'Option 3']} />
// GOOD
const SELECT_OPTIONS = ['Option 1', 'Option 2', 'Option 3'];
<Select options={SELECT_OPTIONS} />
// BAD — new function reference every render (breaks React.memo on child)
<Button onClick={() => handleClick(item.id)} />
// GOOD — use useCallback when passing to memoized components
const handleItemClick = useCallback(() => handleClick(item.id), [item.id]);
<Button onClick={handleItemClick} />
Mistake 2: Putting Everything in One Giant Component
When a component holds too much state, every state change re-renders the entire thing — including parts that don't care about that state.
// BAD — one component, all state, everything re-renders on any change
function Dashboard() {
const [activeTab, setActiveTab] = useState('overview');
const [searchQuery, setSearchQuery] = useState('');
const [filters, setFilters] = useState({});
const [sortOrder, setSortOrder] = useState('desc');
// When searchQuery changes, activeTab rendering code re-runs too
return (
<div>
<TabBar active={activeTab} onChange={setActiveTab} />
<SearchBar value={searchQuery} onChange={setSearchQuery} />
<FilterPanel filters={filters} onChange={setFilters} />
<DataTable
query={searchQuery}
filters={filters}
sort={sortOrder}
/>
</div>
);
}
// GOOD — split state to co-locate it with what uses it
function Dashboard() {
const [activeTab, setActiveTab] = useState('overview');
return (
<div>
<TabBar active={activeTab} onChange={setActiveTab} />
<SearchSection /> {/* owns its own search state */}
</div>
);
}
// SearchSection owns its own state — Dashboard doesn't re-render when it changes
function SearchSection() {
const [searchQuery, setSearchQuery] = useState('');
const [filters, setFilters] = useState({});
const [sortOrder, setSortOrder] = useState('desc');
return (
<>
<SearchBar value={searchQuery} onChange={setSearchQuery} />
<FilterPanel filters={filters} onChange={setFilters} />
<DataTable query={searchQuery} filters={filters} sort={sortOrder} />
</>
);
}
State colocation is the performance technique that pays off the most, in my experience.
Mistake 3: Missing or Wrong Keys in Lists
Using wrong keys in lists causes React to destroy and recreate DOM nodes instead of reusing them. This is particularly bad for lists of inputs or animated elements.
// BAD — index as key: causes issues when list reorders or updates
{items.map((item, index) => (
<ListItem key={index} item={item} />
))}
// BAD — no key at all (React will warn, but the bugs are subtle)
{items.map((item) => (
<ListItem item={item} />
))}
// GOOD — stable unique identifier
{items.map((item) => (
<ListItem key={item.id} item={item} />
))}
Index keys cause specific problems:
- When items are reordered, React matches by index — items get the wrong state
- Controlled inputs retain their values when their sibling items change positions
- Animations play incorrectly because React thinks it's the same element
// Real bug example: checkboxes get wrong state when list is reordered
// With index keys
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} /> // checked state follows position, not item
))}
// Fixed with stable ID
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} /> // checked state follows the item
))}
The React hooks notes cover how keys interact with component state and lifecycle.
Mistake 4: Expensive Computations Without Memoization
Some computations are genuinely expensive and worth memoizing. The mistake is not knowing which ones those are.
// SLOW — filter + sort runs on every render, even when data hasn't changed
function ProductList({ products, searchTerm, sortBy }) {
const filtered = products
.filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()))
.sort((a, b) => {
if (sortBy === 'price') return a.price - b.price;
return a.name.localeCompare(b.name);
});
return filtered.map(p => <ProductCard key={p.id} product={p} />);
}
// FAST — only recomputes when products, searchTerm, or sortBy actually changes
function ProductList({ products, searchTerm, sortBy }) {
const filtered = useMemo(() => {
return products
.filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()))
.sort((a, b) => {
if (sortBy === 'price') return a.price - b.price;
return a.name.localeCompare(b.name);
});
}, [products, searchTerm, sortBy]);
return filtered.map(p => <ProductCard key={p.id} product={p} />);
}
How do you know if a computation is "expensive enough" to memoize? Measure it:
// Quick benchmark in development
console.time('filter-sort');
const result = expensiveOperation(data);
console.timeEnd('filter-sort');
// If it's consistently > 1ms with real data, consider memoizing
Don't memoize primitive values, simple property lookups, or anything that takes under 0.1ms. The memoization overhead isn't worth it.
Mistake 5: Not Using React.memo for Pure Components
Components that receive the same props shouldn't re-render. React.memo wraps a component so it only re-renders when props actually change.
// Without memo: re-renders whenever the parent renders, regardless of props
function StatCard({ title, value, icon }) {
return (
<div className="stat-card">
<span className="stat-icon">{icon}</span>
<div>
<p className="stat-title">{title}</p>
<p className="stat-value">{value}</p>
</div>
</div>
);
}
// With memo: only re-renders when title, value, or icon actually changes
const StatCard = React.memo(function StatCard({ title, value, icon }) {
return (
<div className="stat-card">
<span className="stat-icon">{icon}</span>
<div>
<p className="stat-title">{title}</p>
<p className="stat-value">{value}</p>
</div>
</div>
);
});
React.memo uses shallow comparison by default. For complex props, provide a custom comparison:
const DataTable = React.memo(
function DataTable({ columns, rows, onRowClick }) {
// ...
},
(prevProps, nextProps) => {
// Return true if rendering should be SKIPPED (props are equal)
return (
prevProps.rows === nextProps.rows &&
prevProps.columns === nextProps.columns
// onRowClick intentionally omitted — we handle this with useCallback
);
}
);
Note: React 19's compiler handles many of these cases automatically. But understanding manual memoization still matters for complex cases and for working in React 17/18 codebases.
Mistake 6: useEffect with Missing or Incorrect Dependencies
Wrong effect dependencies cause either stale closures (bug) or infinite loops (visible crash). Both are performance problems, but stale closures are worse because they're silent.
// BUG — stale closure: count never updates in the interval
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // always logs 0 — stale closure
setCount(count + 1); // always sets to 1
}, 1000);
return () => clearInterval(id);
}, []); // empty deps = runs once, count is frozen at 0
}
// FIX — use functional update form to avoid the dependency
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1); // always uses current state
}, 1000);
return () => clearInterval(id);
}, []); // now legitimately safe with empty deps
}
// INFINITE LOOP — object in deps causes new reference every render
function UserData({ userId }) {
const [user, setUser] = useState(null);
const options = { headers: { 'x-user-id': userId } }; // new ref every render
useEffect(() => {
fetchUser(userId, options).then(setUser);
}, [userId, options]); // options changes every render → infinite loop
// FIX: move options inside the effect, or useMemo for stable reference
useEffect(() => {
const opts = { headers: { 'x-user-id': userId } };
fetchUser(userId, opts).then(setUser);
}, [userId]); // only depends on userId
}
The JavaScript ES6 cheatsheet covers closures in depth if the stale closure concept is new to you.
Mistake 7: Loading Everything at Once (No Code Splitting)
Loading your entire JavaScript bundle up front delays the initial render. React's lazy and Suspense give you code splitting with minimal setup.
// BAD — entire app in one bundle
import Dashboard from './pages/Dashboard';
import Analytics from './pages/Analytics';
import Settings from './pages/Settings';
// GOOD — split by route
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Router>
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</Router>
);
}
You can also lazy-load expensive components that aren't immediately visible:
// Lazy load a heavy chart component only when the tab is active
const HeavyChart = lazy(() => import('./HeavyChart'));
function ReportTab({ isActive }) {
if (!isActive) return <div className="tab-placeholder" />;
return (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart />
</Suspense>
);
}
For full-stack React, check the Next.js App Router notes — App Router builds code splitting in at the route level automatically.
Performance Comparison Table
| Mistake | Impact | Fix Complexity | React 19 Compiler Helps? |
|---|---|---|---|
| Inline object/array references | High | Low | Yes, partially |
| Monolithic component state | High | Medium | No |
| Wrong list keys | Medium-High | Low | No |
| Untracked expensive computations | Medium | Low | Yes |
| Missing React.memo | Medium | Low | Yes, mostly |
| Wrong effect dependencies | High (bugs) | Medium | No |
| No code splitting | High (load time) | Low | No |
Quick Profiling Workflow
My process when a component feels slow:
- Open React DevTools Profiler
- Record the interaction that feels slow
- Find the component with highest self-time in the Ranked chart
- Check: is it re-rendering when it shouldn't? (highlight re-renders option)
- Find the cause: prop change, state change, parent re-render
- Apply the appropriate fix from this list
- Record again to verify improvement
The JavaScript React guide covers this workflow in more detail, and the web dev roadmap 2026 puts React performance in the context of broader frontend optimization trends.
Conclusion
Performance optimization in React isn't about applying every technique — it's about finding the actual bottleneck and fixing it precisely. The Profiler is your compass; don't guess.
Start with state colocation and code splitting — they require no performance profiling and always help. Then reach for React.memo, useMemo, and useCallback when the Profiler shows you a genuine problem. Save micro-optimizations for last.
The goal isn't perfectly optimized code. It's fast enough UI that users enjoy. Most of the time, fixing two or three issues from this list gets you there.
FAQs
Should I use useMemo and useCallback everywhere in React? No — this is actually one of the most common mistakes. useMemo and useCallback have their own overhead. Only use them when a computation is genuinely expensive (measurable), when you need referential equality for effect dependencies, or when passing functions to memoized child components.
How do I find which component is causing slowness in React? Open React DevTools, go to the Profiler tab, click Record, interact with the slow part of your app, then stop recording. The flame graph shows you exactly which components rendered and how long each took. Sort by 'self time' to find the actual bottleneck.
Does React 19's compiler fix all performance issues automatically? The React compiler handles many re-render optimizations automatically, but it doesn't fix architecture issues like fetching data in the wrong place, creating new object references in JSX, or key management problems. Understanding these patterns still matters.
Frequently Asked Questions
AiTechWorlds Team
✓ Verified WriterThe AiTechWorlds team is passionate about AI, technology, and education. We create high-quality, research-backed content to help you learn, grow, and succeed in the modern digital world.
Related Articles
5 GraphQL Resolver Best Practices (DataLoader, Error Handling)
Write efficient GraphQL resolvers that don't hammer your database. DataLoader N+1 fix, error handling patterns, auth in context, and resolver performance comparison.
How to Build a Dark Mode Toggle With CSS and JavaScript
Build a dark mode toggle with CSS variables, localStorage persistence, and prefers-color-scheme support. Full code, accessibility tips, and framework comparison included.
GraphQL vs REST: Real-World Performance Test and Benchmarks
Real benchmark results comparing GraphQL and REST APIs on response time, payload size, and network requests — with honest analysis of over-fetching, under-fetching, and when each wins.
Modern JavaScript Features You Must Know in 2026 (ES2026 Guide)
ES2026 brings new JavaScript features worth knowing — from import attributes to pipe operators. Here's every confirmed proposal with code examples and browser support.