React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

react-19compilerauto-memoizationreact-forgetperformancebabel

React Compiler - Automatic memoization that eliminates manual useMemo, useCallback, and React.memo

Recipe

# Install the compiler and its Babel plugin
npm install -D babel-plugin-react-compiler
npm install -D react-compiler-healthcheck  # optional: check compatibility
// babel.config.js
module.exports = {
  plugins: [
    ["babel-plugin-react-compiler", {
      // target defaults to "19" for React 19
    }],
  ],
};
// next.config.js (Next.js 15+)
module.exports = {
  experimental: {
    reactCompiler: true,
  },
};

When to reach for this: Enable the React Compiler for any React 19 project to automatically optimize re-renders. It replaces the need for useMemo, useCallback, and React.memo in the vast majority of cases.

Working Example

// Before: Manual memoization everywhere
import { useMemo, useCallback, memo } from "react";
 
type Todo = { id: string; text: string; completed: boolean };
 
const TodoItem = memo(function TodoItem({
  todo,
  onToggle,
}: {
  todo: Todo;
  onToggle: (id: string) => void;
}) {
  return (
    <li>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      {todo.text}
    </li>
  );
});
 
function TodoList({ todos }: { todos: Todo[] }) {
  const [filter, setFilter] = useState<"all" | "active" | "completed">("all");
 
  const filteredTodos = useMemo(() => {
    switch (filter) {
      case "active": return todos.filter((t) => !t.completed);
      case "completed": return todos.filter((t) => t.completed);
      default: return todos;
    }
  }, [todos, filter]);
 
  const handleToggle = useCallback((id: string) => {
    // toggle logic
  }, []);
 
  const stats = useMemo(() => ({
    total: todos.length,
    active: todos.filter((t) => !t.completed).length,
    completed: todos.filter((t) => t.completed).length,
  }), [todos]);
 
  return (
    <div>
      <p>{stats.active} items left</p>
      <ul>
        {filteredTodos.map((todo) => (
          <TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
        ))}
      </ul>
    </div>
  );
}
// After: With React Compiler -- no manual memoization needed
import { useState } from "react";
 
type Todo = { id: string; text: string; completed: boolean };
 
function TodoItem({
  todo,
  onToggle,
}: {
  todo: Todo;
  onToggle: (id: string) => void;
}) {
  return (
    <li>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      {todo.text}
    </li>
  );
}
 
function TodoList({ todos }: { todos: Todo[] }) {
  const [filter, setFilter] = useState<"all" | "active" | "completed">("all");
 
  const filteredTodos = (() => {
    switch (filter) {
      case "active": return todos.filter((t) => !t.completed);
      case "completed": return todos.filter((t) => t.completed);
      default: return todos;
    }
  })();
 
  function handleToggle(id: string) {
    // toggle logic
  }
 
  const stats = {
    total: todos.length,
    active: todos.filter((t) => !t.completed).length,
    completed: todos.filter((t) => t.completed).length,
  };
 
  return (
    <div>
      <p>{stats.active} items left</p>
      <ul>
        {filteredTodos.map((todo) => (
          <TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
        ))}
      </ul>
    </div>
  );
}

What this demonstrates:

  • The "before" version requires useMemo, useCallback, and React.memo to avoid unnecessary re-renders
  • The "after" version is plain, natural React code -- the compiler adds memoization at build time
  • The code is simpler, easier to read, and equally performant

Deep Dive

How It Works

  • The React Compiler is a build-time tool (Babel plugin) that analyzes your components and hooks, then automatically inserts memoization.
  • It understands React's rules of hooks and the rules of React (pure rendering, immutable props/state) to determine what values can be safely cached.
  • The compiler transforms your code to memoize: JSX elements, expensive computations, callback functions, and component re-renders -- roughly equivalent to wrapping things in useMemo, useCallback, and React.memo where appropriate.
  • It performs fine-grained memoization -- it can memoize individual JSX subtrees within a component, not just the entire return value.
  • The compiler targets React 19 by default and uses React 19's internal cache API. For React 17 and 18, set target: "17" or target: "18" and install the react-compiler-runtime package.
  • If the compiler detects code that violates React's rules (e.g., mutating props, side effects during render), it skips that component and leaves it unoptimized. It does not break your code.
  • You can use the "use no memo" directive at the top of a function to opt out a specific component or hook from compiler optimization.

Variations

Gradual rollout with opt-in mode:

// babel.config.js -- only compile files with "use memo" directive
module.exports = {
  plugins: [
    ["babel-plugin-react-compiler", {
      compilationMode: "annotation",  // only compile "use memo" functions
    }],
  ],
};
// Only this component is compiled
function OptimizedList({ items }: { items: string[] }) {
  "use memo";
  return (
    <ul>
      {items.map((item) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
}

Opting out a specific component:

function LegacyComponent({ data }: { data: any }) {
  "use no memo";
  // This component will not be optimized by the compiler
  // Useful for code that intentionally breaks React rules
  externalMutableStore.value = data;
  return <div>{data.label}</div>;
}

Health check before adopting:

# Run the health check to see how compatible your codebase is
npx react-compiler-healthcheck --verbose

ESLint plugin for rule validation:

npm install -D eslint-plugin-react-compiler
// eslint.config.js
import reactCompiler from "eslint-plugin-react-compiler";
 
export default [
  {
    plugins: { "react-compiler": reactCompiler },
    rules: {
      "react-compiler/react-compiler": "error",
    },
  },
];

TypeScript Notes

  • The compiler works with TypeScript out of the box -- it runs after TypeScript compilation (in the Babel pipeline) or integrates with SWC in Next.js.
  • No type changes are needed. The compiler's transformations are transparent to the type system.
  • "use memo" and "use no memo" are string literal directives (like "use strict") and do not need type declarations.

Gotchas

  • Mutating objects or arrays during render -- The compiler assumes pure rendering. Mutating props, state, or variables that are shared across renders causes incorrect caching. Fix: Follow React's rules -- always create new objects/arrays instead of mutating. The ESLint plugin catches most violations.
  • Side effects during render -- Code like console.log(Date.now()) or modifying external variables during render may produce stale results when the compiler caches the output. Fix: Move side effects into useEffect or event handlers.
  • External mutable stores -- Reading from mutable global variables or stores that React does not track (e.g., a plain global object) may break under compiler optimization. Fix: Use useSyncExternalStore for external mutable state.
  • Existing useMemo/useCallback not harmful -- If you already have manual memoization, the compiler works with it. You do not need to remove it before enabling the compiler. Fix: Optionally remove manual memoization for cleaner code, but it is not required.
  • Not all code is compiled -- Components that violate React's rules are silently skipped. You may not notice that a component is unoptimized. Fix: Use eslint-plugin-react-compiler to catch issues and check the compiler's build output for skipped components.
  • Build time increase -- The compiler adds analysis time to your build. On very large codebases this may be noticeable. Fix: Use the compilationMode: "annotation" option to compile only opted-in components during migration.

Alternatives

ApproachWhen to choose
React CompilerDefault for React 19 projects -- automatic, zero-effort optimization
Manual useMemo / useCallbackReact 18 or when you need explicit control over what is memoized
React.memoReact 18 or for class components the compiler does not handle
Million.jsAlternative compiler focused on virtual DOM diffing optimization
Manual component splittingIsolating expensive subtrees when compiler or memoization is insufficient
"use no memo" directiveOpting out specific components from compiler optimization

FAQs

What does the React Compiler do at a high level?
  • It is a build-time Babel plugin that analyzes components and hooks, then automatically inserts memoization
  • It replaces the need for manual useMemo, useCallback, and React.memo in most cases
  • It performs fine-grained memoization -- it can memoize individual JSX subtrees, not just the entire return value
How do I enable the React Compiler in a Next.js project?
// next.config.js
module.exports = {
  experimental: {
    reactCompiler: true,
  },
};

For non-Next.js projects, install babel-plugin-react-compiler and add it to your Babel config.

Do I need to remove existing useMemo, useCallback, and React.memo calls?
  • No. The compiler works alongside existing manual memoization
  • You can optionally remove them for cleaner code, but it is not required
  • The compiler will not conflict with or double-memoize these calls
What happens if a component violates React's rules (e.g., mutates props)?
  • The compiler silently skips that component and leaves it unoptimized
  • It does not break your code -- the component renders as if the compiler was not enabled
  • Use eslint-plugin-react-compiler to detect which components are being skipped and why
How do you opt out a specific component from compiler optimization?
function LegacyComponent({ data }) {
  "use no memo";
  // This component will not be optimized
  externalStore.value = data;
  return <div>{data.label}</div>;
}

The "use no memo" directive is placed at the top of the function body.

How do you gradually roll out the compiler with opt-in mode?
// babel.config.js
module.exports = {
  plugins: [
    ["babel-plugin-react-compiler", {
      compilationMode: "annotation",
    }],
  ],
};

Then add "use memo" at the top of functions you want to compile. Only annotated functions are optimized.

How do you check if your codebase is compatible with the compiler?
npx react-compiler-healthcheck --verbose

This scans your codebase and reports which components follow React's rules and which may cause issues.

Can the compiler be used with React 17 or 18?
  • Yes. Set target: "17" or target: "18" in the plugin config
  • You must also install the react-compiler-runtime package
  • The compiler targets React 19 by default
Gotcha: Why does a console.log inside my component show stale values after enabling the compiler?
  • The compiler caches render output when inputs have not changed
  • Side effects like console.log(Date.now()) during render may produce stale results
  • Move side effects into useEffect or event handlers where they belong
Gotcha: My component reads from a global mutable variable and it returns stale data with the compiler. Why?
  • The compiler assumes pure rendering and does not track external mutable stores
  • Reading from plain global objects that React does not manage breaks under compiler optimization
  • Use useSyncExternalStore for external mutable state so React can track changes
Does the compiler affect TypeScript types or require type changes?
  • No. The compiler's transformations are transparent to the type system
  • It runs after TypeScript compilation (in the Babel pipeline) or integrates with SWC in Next.js
  • "use memo" and "use no memo" are string literal directives and do not need type declarations
Does the React Compiler increase build times?
  • Yes, it adds analysis time to the build
  • On very large codebases, this may be noticeable
  • Use compilationMode: "annotation" to compile only opted-in components during an incremental migration
  • Overview -- Full list of React 19 features
  • Server Components -- Server Components do not ship JS so memoization is less relevant there
  • Ref as Prop -- Another API simplification in React 19