React Compiler: Automatic Memoization and the End of useMemo Boilerplate (2026)

React Compiler: Automatic Memoization and the End of useMemo Boilerplate (2026)

Mykola Nesterchuk - March 16, 2026

#react
#performance
#internals

Introduction

For years, writing performant React meant wrapping things in useMemo, stabilizing callbacks with useCallback, and reaching for React.memo whenever a list item felt slow. The mental overhead was real - dependency arrays, stale closures, and the constant question: "did I memoize this correctly?"

React Compiler v1.0, released in October 2025, changes that calculus. It is a build-time tool that analyzes your components and inserts memoization automatically. In most cases, you write plain code and the compiler handles the rest.

This article covers what the compiler actually does under the hood, how to enable it, when manual memoization still makes sense, and a practical migration path for existing codebases.

What the Compiler Actually Does

The React Compiler is a Babel/SWC plugin that transforms your source code before it reaches the browser. It reads each component, builds a dependency graph of every value and function, and generates optimized output - similar to what you would write manually with useMemo and useCallback, but more precise.

Take a simple example:

// What you write function Dashboard({ data }) { const summary = computeExpensiveSummary(data); return <SummaryCard data={summary} />; }

The compiler transforms this roughly into:

// What the compiler generates function Dashboard({ data }) { const summary = useMemo(() => computeExpensiveSummary(data), [data]); return <SummaryCard data={summary} />; }

But it goes further than what most developers write manually. Functions passed as props get stable references automatically. Child components are treated as effectively memoized when their inputs do not change. The compiler applies memoization at the granularity of individual expressions, not just at the hook call level.

The key requirement: your components must be pure. The compiler assumes that render functions have no side effects and that the same inputs always produce the same output. If your component mutates external state during render, the compiler either skips it or produces incorrect output.

How to Enable It

For new projects using Next.js (where the compiler ships as the default from Next.js 15+), it is already on. For existing projects with Vite or a custom build setup:

npm install -D babel-plugin-react-compiler eslint-plugin-react-compiler

For Vite:

// vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [ react({ babel: { plugins: ['babel-plugin-react-compiler'], }, }), ], });

For Next.js (if not already enabled):

// next.config.ts const nextConfig = { experimental: { reactCompiler: true, }, }; export default nextConfig;

Once enabled, open React DevTools. Components optimized by the compiler show a small badge. If a component is not optimized, the compiler skipped it - usually because it detected a purity violation.

Verifying Compiler Output

The ESLint plugin flags code that the compiler cannot safely optimize:

npm install -D eslint-plugin-react-compiler
// .eslintrc { "plugins": ["react-compiler"], "rules": { "react-compiler/react-compiler": "error" } }

Treat compiler lint errors as blockers, not warnings. They surface exactly the patterns that prevent optimization.

When You Still Need useMemo and useCallback

The compiler handles the vast majority of cases, but not all of them. There are three categories where manual memoization still makes sense.

Third-party libraries with interior mutability

Some libraries expose functions whose reference stays the same but whose return value changes internally. react-hook-form's watch function is a known example:

// The compiler cannot detect that watch() has interior mutability const { watch } = useForm(); // You need to memoize explicitly here const values = useMemo(() => watch(), [watch]);

Drag-and-drop libraries like react-dnd also depend on reference equality for registering handlers. Removing memoization there causes handlers to re-register on every render.

Libraries that compare props by identity

Chart libraries, map libraries, and some animation libraries accept callbacks or config objects and compare them by reference to decide whether to re-render. If you pass an unstabilized function to these, the library will re-render on every React render regardless of what the compiler does:

// Chart.js and similar libs compare this by identity const chartOptions = useMemo(() => ({ scales: { y: { beginAtZero: true } }, onClick: handleBarClick, }), [handleBarClick]);

Genuinely expensive computations you want to control explicitly

For calculations that are measurably slow - filtering tens of thousands of items, running complex transforms - keeping an explicit useMemo documents intent and gives you precise control over when recomputation happens:

// Intentional - expensive and worth documenting const sortedAndFilteredRows = useMemo(() => rows .filter(row => matchesAllFilters(row, activeFilters)) .sort(comparator), [rows, activeFilters, comparator] );

The rule of thumb: if you are adding useMemo defensively - "just in case" - remove it and let the compiler handle it. If you are adding it because you measured a real cost or because a third-party library requires it, keep it.

The "use no memo" Escape Hatch

If the compiler optimizes a component incorrectly - which can happen with complex state patterns it cannot statically analyze - you can opt out:

function ComplexComponent({ data }) { 'use no memo'; // compiler skips this component entirely // ... }

Use this sparingly. It exists for edge cases, not as a general escape from fixing purity violations.

Migrating an Existing Codebase

Do not try to migrate everything at once. The compiler is additive - it works alongside existing memoization and skips components it cannot optimize safely.

A practical order:

  1. Enable the ESLint plugin first - before touching any code. Fix the violations it flags. Each violation is a purity issue or a rules-of-hooks issue that would prevent the compiler from running.

  2. Enable the compiler with no other changes. Check DevTools to see what gets the badge.

  3. Remove defensive memoization - start with useMemo calls that wrap cheap operations, and useCallback calls that only exist to satisfy React.memo on a child. The compiler covers these automatically.

  4. Profile before and after - use the React DevTools Profiler to confirm that the components you changed are not re-rendering more than before.

  5. Keep targeted memoization - for the third-party library cases and expensive computations described above.

// Before migration - defensive boilerplate everywhere function UserList({ users, onSelect }) { const sorted = useMemo( () => [...users].sort((a, b) => a.name.localeCompare(b.name)), [users] ); const handleSelect = useCallback( (id: string) => onSelect(id), [onSelect] ); return ( <ul> {sorted.map(user => ( <UserItem key={user.id} user={user} onSelect={handleSelect} /> ))} </ul> ); } // After migration - the compiler handles stability function UserList({ users, onSelect }) { const sorted = [...users].sort((a, b) => a.name.localeCompare(b.name)); return ( <ul> {sorted.map(user => ( <UserItem key={user.id} user={user} onSelect={(id) => onSelect(id)} /> ))} </ul> ); }

The second version is both shorter and at least as performant - the compiler generates stable references for sorted and the inline arrow function automatically.

What This Means for How You Write Components

The compiler shifts the mental model: instead of writing components and then layering memoization on top, you write components that are pure and let performance follow from that.

In practice this means:

  • No side effects in render. Move console.log, mutations, and network calls into useEffect or event handlers.
  • No mutating props or state in place. Always create new objects and arrays when data changes.
  • No external mutable singletons read during render. Values accessed in render should come from props, state, or constants.

Most of these are already part of the Rules of React. The compiler just enforces them more strictly and rewards compliance with automatic optimization.

Connection to React's Rendering Model

Understanding the compiler is easier if you already know how React's render pipeline works. When a component re-renders, React calls the component function and reconciles the output against the previous virtual DOM. The compiler does not change this model - it makes the inputs to that process more stable, so reconciliation finds fewer differences and skips more work.

The commit phase, useEffect timing, and the Server/Client boundary in Next.js App Router are all unaffected. The compiler is purely a render-phase optimization.

Summary

React Compiler v1.0 automates most of what developers used to do manually with useMemo, useCallback, and React.memo.

  • Enable it in your build config - Next.js has it by default, Vite needs a plugin
  • Write pure components - no side effects or mutations in render
  • Use the ESLint plugin to catch purity violations before they become runtime bugs
  • Remove defensive memoization - the compiler covers the common cases
  • Keep manual memoization for third-party library interop and genuinely expensive computations
  • Migrate incrementally - the compiler is additive and coexists with existing code

The goal is not to delete every useMemo in your codebase. It is to stop writing them by default and only reach for them when you have a specific reason.

Table of Contents