Skip to content
Codeloom
React

React Compiler and Automatic Memoization

Understand how the React Compiler works, what automatic memoization means, and how it eliminates the need for useMemo, useCallback, and React.memo.

·6 min read · By Codeloom
Intermediate 12 min read

What you'll learn

  • What the React Compiler does under the hood
  • How automatic memoization replaces manual optimization
  • The rules your code must follow
  • How to adopt the compiler incrementally
  • Debugging compiler output

Prerequisites

  • Comfortable with React components and hooks
  • Familiarity with useMemo and useCallback

The React Compiler (previously known as React Forget) is a build-time tool that analyzes your components and hooks, then automatically inserts memoization where it helps. Instead of you deciding where to place useMemo, useCallback, or React.memo, the compiler figures it out for you. The result is code that is both simpler to write and faster to run.

Why Manual Memoization Falls Short

Developers often memoize too little or too much. Missing a single dependency causes stale data. Adding useMemo around a cheap computation adds overhead without benefit. The cognitive load is real: every new value or callback forces you to ask whether it needs wrapping.

The compiler removes this burden by treating memoization as a compilation concern rather than an authoring concern.

How the Compiler Works

At build time, the compiler parses each component and hook function. It builds a dependency graph of every value, tracking which variables depend on which others. Then it groups values into memoization blocks. If a group of values depends only on inputs that have not changed since the last render, the compiler wraps them so React can skip recomputation.

Consider this component:

function ProductCard({ product, onAddToCart }) {
  const discountedPrice = product.price * (1 - product.discount);
  const formattedPrice = `$${discountedPrice.toFixed(2)}`;

  const handleClick = () => {
    onAddToCart(product.id, discountedPrice);
  };

  return (
    <div>
      <h3>{product.name}</h3>
      <p>{formattedPrice}</p>
      <button onClick={handleClick}>Add to Cart</button>
    </div>
  );
}

Without the compiler, discountedPrice, formattedPrice, and handleClick are recalculated on every render, even if product and onAddToCart have not changed. The compiler detects that all three depend only on those two props and wraps them in a cache block. The compiled output is conceptually similar to:

function ProductCard({ product, onAddToCart }) {
  const $ = useMemoCache(4);

  let discountedPrice, formattedPrice, handleClick;

  if ($[0] !== product || $[1] !== onAddToCart) {
    discountedPrice = product.price * (1 - product.discount);
    formattedPrice = `$${discountedPrice.toFixed(2)}`;
    handleClick = () => onAddToCart(product.id, discountedPrice);
    $[0] = product;
    $[1] = onAddToCart;
    $[2] = formattedPrice;
    $[3] = handleClick;
  } else {
    formattedPrice = $[2];
    handleClick = $[3];
  }

  return (
    <div>
      <h3>{product.name}</h3>
      <p>{formattedPrice}</p>
      <button onClick={handleClick}>Add to Cart</button>
    </div>
  );
}

You never write this version. The compiler generates it from your clean, readable source code.

The Rules of React

The compiler relies on your code following the Rules of React. If you break them, the compiler either skips the component or produces incorrect output.

The key rules are:

  • Components and hooks must be pure during render. No side effects, no mutations of external variables, no reading from mutable globals.
  • Props and state are immutable. Never mutate them directly. Always return new objects from state updates.
  • Hook calls must be unconditional. No hooks inside loops, conditions, or nested functions.
  • Hook return values and arguments are immutable. Do not mutate the array returned by useState, for example.
// Bad - mutates during render
function Counter({ items }) {
  const sorted = items.sort(); // mutates the prop array
  return <ul>{sorted.map(i => <li key={i}>{i}</li>)}</ul>;
}

// Good - creates a new array
function Counter({ items }) {
  const sorted = [...items].sort();
  return <ul>{sorted.map(i => <li key={i}>{i}</li>)}</ul>;
}

Setting Up the Compiler

The compiler ships as a Babel plugin. Install it and add it to your build configuration.

npm install -D babel-plugin-react-compiler

For a Vite project, add it to your Babel configuration:

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: ['babel-plugin-react-compiler'],
      },
    }),
  ],
});

Next.js has built-in support. Enable it in next.config.js:

// next.config.js
module.exports = {
  experimental: {
    reactCompiler: true,
  },
};

Incremental Adoption

You do not have to compile your entire codebase at once. The compiler supports an opt-in mode where only components and hooks with a specific directive are compiled.

// Only this component will be compiled
function Dashboard() {
  'use memo';
  const stats = computeStats(data);
  return <StatsGrid stats={stats} />;
}

You can also use an opt-out directive for components that intentionally break the rules (legacy code, third-party integrations):

function LegacyWidget({ config }) {
  'use no memo';
  // This component mutates config internally
  externalLib.init(config);
  return <div ref={el => externalLib.mount(el)} />;
}

Debugging Compiler Output

The compiler provides an ESLint plugin that warns you when your code violates the Rules of React. Install it to catch issues early.

npm install -D eslint-plugin-react-compiler
// .eslintrc.js
module.exports = {
  plugins: ['react-compiler'],
  rules: {
    'react-compiler/react-compiler': 'error',
  },
};

When something behaves unexpectedly, you can inspect the compiled output. The compiler adds comments to the generated code showing which values were grouped into cache slots. In development mode, React DevTools marks compiled components with a badge.

What You Can Remove

Once the compiler is active and your components follow the rules, you can safely remove:

  • useMemo calls for computed values
  • useCallback wrappers around event handlers
  • React.memo wrappers around child components

The compiler handles all three automatically and often does a better job because it can memoize at a finer granularity than component-level React.memo.

// Before - manual memoization
const MemoizedChild = React.memo(function Child({ data, onClick }) {
  const processed = useMemo(() => transform(data), [data]);
  const handler = useCallback(() => onClick(processed), [onClick, processed]);
  return <button onClick={handler}>{processed.label}</button>;
});

// After - let the compiler handle it
function Child({ data, onClick }) {
  const processed = transform(data);
  const handler = () => onClick(processed);
  return <button onClick={handler}>{processed.label}</button>;
}

When to Wait

The compiler is production-ready but still evolving. Hold off if your codebase heavily relies on mutation patterns during render, uses class components extensively, or depends on libraries that mutate props. Run the ESLint plugin first to gauge how much of your code is already compliant.

For new projects, adopt the compiler from day one. For existing projects, enable it incrementally and let the linter guide your cleanup. Either way, the future of React performance is automatic.