Homechevron_rightBlogchevron_rightFrontend
Frontendschedule7 min read28 February 2025

React Performance Patterns in 2025

memo, useMemo, useCallback, Suspense, and the new React Compiler — which optimisations actually matter and which are premature, with real profiler numbers.

ReactNext.jsPerformanceTypeScriptFrontend
smart_toy

AI-Assisted Content. This article was generated with AI and reviewed for accuracy based on real engineering experience. Code examples are tested and production-relevant.

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.