Optimizing React Renders: Patterns I Actually Use
Real-world patterns from my projects β memo, lazy loading, virtualization, and when NOT to optimize.
The Golden Rule: Profile First
Before touching memo or useMemo, open React DevTools Profiler and confirm you have an actual problem. Most apps never need manual memoization β Reactβs reconciler is already very fast.
That said, here are the patterns I reach for when profiling reveals genuine pain.
1. React.memo for Pure Components
Wrap components whose props rarely change but that live inside frequently re-rendering parents:
const ProjectCard = React.memo(({ project }: { project: Project }) => (
<div className="card">
<h3>{project.title}</h3>
<p>{project.description}</p>
</div>
));
The rule: memo pays off when the componentβs render cost > comparison cost. Donβt wrap <div> wrappers β wrap heavy components with expensive children or DOM.
2. useMemo for Expensive Derivations
// Without memo: sorted on every render, even on unrelated state changes
const sorted = projects.sort((a, b) => b.stars - a.stars);
// With memo: only re-sorts when projects changes
const sorted = useMemo(
() => [...projects].sort((a, b) => b.stars - a.stars),
[projects]
);
Keep memo dependencies tight. A missing dependency is a stale closure bug; a spurious dependency defeats the optimization.
3. useCallback for Stable Handler References
Only matters when passing callbacks to memoized children:
const handleFilter = useCallback((tag: string) => {
setFilter(tag);
}, []); // stable β setFilter identity is guaranteed by React
4. Lazy + Suspense for Code Splitting
Heavy components (charts, rich text editors, 3D scenes) should be lazily imported:
const GitHubCalendar = lazy(() => import('./GitHubCalendar'));
function StatsSection() {
return (
<Suspense fallback={<Skeleton />}>
<GitHubCalendar username="supremovb" />
</Suspense>
);
}
This keeps the initial bundle small and defers heavy parsing until the component scrolls into view.
5. Virtualization for Long Lists
If youβre rendering > 100 items, virtualize. I use @tanstack/react-virtual β itβs framework-agnostic and tiny:
const rowVirtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 80,
});
Only the visible rows hit the DOM. Scrolling 10k items at 60 fps becomes trivial.
What I Donβt Do
- Wrapping every component in
memoβjust in caseβ useMemoon primitive values- Splitting components into micro-files to βforceβ memoization boundaries
- Micro-optimizing before the feature works
Profile, measure, optimize the bottleneck, measure again. The end.
Found this useful? Share it.