Introduction
React's performance story has evolved significantly. With the React Compiler landing in React 19, some of the manual memoisation patterns we've relied on are becoming obsolete. Here's what still matters in 2025.
Rule #1: Profile First
Don't memoize speculatively. Open React DevTools Profiler, record a slow interaction, and find actual expensive renders before touching the code.
The most common culprit: a parent re-rendering and passing new object/function references to pure children.
When memo Actually Helps
// ✓ Worth it — expensive render, stable props
const ProjectCard = memo(({ project }: { project: Project }) => {
return (
<div>
<h3>{project.name}</h3>
<TechBadgeList technologies={project.technologies} />
</div>
);
});
// ❌ Pointless — component is trivially fast
const Label = memo(({ text }: { text: string }) => <span>{text}</span>);
useCallback — Only for Stable References
// ✓ Needed — passed to memo'd child or used as useEffect dep
const handleSubmit = useCallback(async (data: FormData) => {
await api.post('/contact', data);
}, []); // stable — no deps
// ❌ Unnecessary — not passed anywhere, costs more than it saves
const formatDate = useCallback((d: Date) => d.toISOString(), []);
useMemo for Expensive Computation
// ✓ Worth it — filtering/sorting 1000+ items
const filteredProjects = useMemo(
() => projects.filter(p => p.tags.includes(activeTag)).sort(byDate),
[projects, activeTag]
);
// ❌ Overkill — object creation is not the bottleneck
const style = useMemo(() => ({ color: 'teal' }), []);
Suspense + Streaming in Next.js App Router
The biggest performance win in 2025 isn't memoization — it's Suspense boundaries that let the browser paint fast content while waiting for slow data:
// page.tsx
export default function BlogPage() {
return (
<main>
<BlogHeader /> {/* Renders immediately */}
<Suspense fallback={<ArticlesSkeleton />}>
<ArticlesFeed /> {/* Streams in when ready */}
</Suspense>
</main>
);
}
The React Compiler (React 19)
The compiler automatically inserts memoization at the right granularity — removing the need for most manual memo, useCallback, and useMemo calls. Enable it in Next.js:
// next.config.ts
const nextConfig = {
experimental: {
reactCompiler: true,
},
};
After enabling, audit your codebase and remove most manual memoization — the compiler handles it more precisely than humans do.
What Actually Moves the Needle
| Technique | Impact | When | |---|---|---| | Suspense + streaming | Very High | Data-heavy pages | | Virtualisation (react-window) | Very High | Lists > 200 items | | Code splitting + lazy() | High | Large page bundles | | memo on expensive components | Medium | Profiler-confirmed | | useCallback for stable refs | Low-Medium | Memo'd child deps | | useMemo for heavy computation | Low-Medium | Profiler-confirmed | | Manual memo everywhere | Negative | Never |
Summary
Profile before you optimise. In 2025, the highest-value React performance wins come from architecture (Suspense, streaming, code splitting) not micro-optimisation. Let the React Compiler handle the rest.