Why I stopped using React.memo() everywhere and started measuring first
React.memo() feels like free performance, but it often creates more problems than it solves.
Why I stopped using React.memo() everywhere and started measuring first
I used to wrap every component in React.memo(). It felt like free performance -- prevent unnecessary re-renders, make the app faster, what could go wrong? Then I built a dashboard with 30+ memoized components and discovered my "optimization" was actually making the app slower. The memoization overhead was costing more than the re-renders I was preventing, and I'd created a debugging nightmare where props changes did not trigger updates like I expected.
The wake-up call came when I was troubleshooting why a user's profile picture would not update after upload. I spent two hours tracing through state updates, API calls, and component lifecycles before realizing that my overly aggressive memoization was blocking a legitimate re-render. The component was comparing the old and new user objects with shallow equality, but the image URL was nested three levels deep in the user object.
That's when I learned the difference between premature optimization and measured optimization. React.memo() is a powerful tool, but like any optimization, it should be applied strategically after identifying actual performance bottlenecks.
The React.memo() Trap
Here's what my old component pattern looked like:
import React, { memo } from 'react';
interface UserCardProps {
user: User;
onEdit: (id: string) => void;
settings: AppSettings;
}
// Old approach: memo everything "just in case"
const UserCard = memo(({ user, onEdit, settings }: UserCardProps) => {
return (
<div className="user-card">
<img src={user.profile.avatar} alt={user.name} />
<h3>{user.name}</h3>
<button onClick={() => onEdit(user.id)}>Edit</button>
</div>
);
});This looks harmless, but I was creating several problems:
- Comparison overhead: React.memo() runs a shallow comparison on every render, which costs CPU time
- False negatives: New object references (like callback functions) break memoization
- Deep equality issues: Nested object changes get missed by shallow comparison
- Debugging complexity: Components do not re-render when you expect them to
When React.memo() Actually Helps
After profiling my apps with React DevTools, I discovered that memoization only provides meaningful benefits in specific scenarios:
- Expensive render functions that do heavy calculations or render many child components
- Components that receive the same props frequently due to parent re-renders
- List items that render hundreds of similar components
Here's an example where React.memo() actually makes sense:
interface ExpensiveListItemProps {
item: ListItem;
index: number;
}
// Good use case: expensive computation + stable props
const ExpensiveListItem = memo(({ item, index }: ExpensiveListItemProps) => {
// This calculation runs on every render without memo
const processedData = useMemo(() => {
return heavyDataProcessing(item.data);
}, [item.data]);
return (
<div className="list-item">
<ComplexChart data={processedData} />
<DetailedStats item={item} />
</div>
);
});My New Approach to React Performance
Now I follow a measure-first methodology:
- Start without memoization -- write clean, readable components first
- Profile with React DevTools -- identify actual performance bottlenecks
- Measure before and after -- verify that optimizations actually help
- Use targeted memoization -- wrap only the components that need it
Here's my current workflow for performance optimization:
# 1. Install React DevTools Profiler
npm install --save-dev react-devtools
# 2. Profile the slow interaction
# Open DevTools > Profiler > Record > Perform slow action
# 3. Identify the expensive components
# Look for components that:
# - Render frequently (high bar in flame graph)
# - Have long render times
# - Re-render unnecessarilyBetter Alternatives to Blanket Memoization
Instead of wrapping everything in React.memo(), I now focus on these strategies:
- Lift expensive state up -- move state closer to where it's actually needed
- Split components strategically -- separate frequently changing parts from stable parts
- Use callback stability -- useCallback for event handlers passed to memoized children
- Optimize data structures -- normalize data to prevent deep object comparisons
// Instead of memoizing everything...
const Dashboard = () => {
const [users, setUsers] = useState<User[]>([]);
const [filters, setFilters] = useState<Filters>({});
// Split into separate components based on update frequency
return (
<div>
<FilterPanel filters={filters} onFiltersChange={setFilters} />
<UserList users={users} />
<UserStats users={users} />
</div>
);
};The key insight is that React is already fast for most use cases. The virtual DOM and reconciliation algorithm are optimized for the common case where most components do not need to re-render. Adding memoization everywhere fights against these optimizations and often makes things worse.
Now I only reach for React.memo() when profiling shows a clear benefit, and I measure the impact to make sure it's actually helping. The result is faster apps with less complexity and fewer subtle bugs.