React Compiler

React Compiler (formerly React Forget) is a build-time tool that automatically adds memoization to your components. It analyzes your code and inserts the equivalent of useMemo, useCallback, and React.memo where needed, so you never have to write them yourself.

What It Does

Without the compiler, React re-renders child components whenever a parent re-renders, even if props haven’t changed. You had to manually prevent this:

// Before: manual memoization everywhere
const MemoizedChild = React.memo(function Child({ items }: { items: Item[] }) {
  const sorted = useMemo(
    () => items.sort((a, b) => a.name.localeCompare(b.name)),
    [items]
  );
  const handleClick = useCallback((id: string) => {
    console.log("clicked", id);
  }, []);

  return sorted.map((item) => (
    <Row key={item.id} item={item} onClick={handleClick} />
  ));
});
// After: just write normal code, compiler handles memoization
function Child({ items }: { items: Item[] }) {
  const sorted = items.sort((a, b) => a.name.localeCompare(b.name));
  const handleClick = (id: string) => {
    console.log("clicked", id);
  };

  return sorted.map((item) => (
    <Row key={item.id} item={item} onClick={handleClick} />
  ));
}

The compiler output is equivalent to the manual version - same performance, less code, fewer bugs from missing dependencies.

Performance Impact

In real-world apps, the compiler typically produces:

  • 25-40% fewer re-renders across the component tree
  • Consistent performance without developer effort - junior and senior devs get the same optimization
  • No performance regressions from forgotten useMemo/useCallback calls

Tip: The compiler doesn’t make slow code fast. It prevents unnecessary work. If your component is slow because of an O(n^2) algorithm, the compiler won’t fix that. It fixes the “parent re-rendered so everything below it re-renders” problem.

How to Enable

Vite + React

npm install -D babel-plugin-react-compiler
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

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

Next.js

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  experimental: {
    reactCompiler: true,
  },
};

export default nextConfig;
npm install -D babel-plugin-react-compiler

Remix

// vite.config.ts
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    remix({
      future: {
        unstable_optimizeDeps: true,
      },
      babel: {
        plugins: [["babel-plugin-react-compiler"]],
      },
    }),
  ],
});

What You Can Stop Writing

Once the compiler is enabled, these patterns are unnecessary:

Before (manual)After (compiler handles it)
React.memo(Component)Just write function Component
useMemo(() => expensive, [deps])Just write const result = expensive
useCallback((x) => fn(x), [deps])Just write const handler = (x) => fn(x)
Careful prop reference stabilityPass objects/arrays freely
// You can delete all of this
const MemoizedList = React.memo(({ items }) => { ... });
const sorted = useMemo(() => items.sort(...), [items]);
const onClick = useCallback(() => setOpen(true), []);

// And write this instead
function List({ items }) {
  const sorted = items.sort(...);
  const onClick = () => setOpen(true);
  ...
}

Gotcha: Don’t remove useMemo/useCallback until the compiler is actually enabled and working. The compiler is an optimization, not a requirement. Your code should work correctly with or without it.

Prerequisites: Rules of React

The compiler assumes your code follows the Rules of React. If it doesn’t, the compiler either skips the component or produces incorrect output.

Rules the Compiler Enforces

  1. Components must be pure during render - same props/state must produce the same JSX. No side effects during render.
// Bad: side effect during render
function Bad({ id }: { id: string }) {
  analytics.track("viewed", id); // runs on every render!
  return <div>{id}</div>;
}

// Good: side effect in useEffect
function Good({ id }: { id: string }) {
  useEffect(() => {
    analytics.track("viewed", id);
  }, [id]);
  return <div>{id}</div>;
}
  1. Don’t mutate props, state, or values created during render.
// Bad: mutating during render
function Bad({ items }: { items: Item[] }) {
  items.sort((a, b) => a.name.localeCompare(b.name)); // mutates the prop!
  return <List items={items} />;
}

// Good: create a new array
function Good({ items }: { items: Item[] }) {
  const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
  return <List items={sorted} />;
}
  1. Hooks must follow the rules - top level only, consistent order, use prefix for custom hooks.

Opting Out

If a component can’t follow the rules (legacy code, intentional side effects), opt it out:

// Opt out a single component
/* eslint react-compiler/react-compiler: "off" */
function LegacyComponent() {
  // compiler won't touch this
}

Or configure in the compiler options:

// Compile only specific directories (gradual adoption)
react({
  babel: {
    plugins: [
      [
        "babel-plugin-react-compiler",
        {
          sources: (filename: string) => {
            return filename.includes("src/components");
          },
        },
      ],
    ],
  },
});

ESLint Integration

The compiler comes with an ESLint plugin that catches code the compiler can’t optimize:

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",
    },
  },
];

This flags:

  • Mutations during render
  • Side effects outside useEffect
  • Hook rule violations
  • Non-idempotent render logic

Tip: Enable the ESLint plugin even if you’re not using the compiler yet. It enforces patterns that make your code correct, easier to reason about, and ready for the compiler when you adopt it.

Verifying It Works

Use React DevTools to verify the compiler is active:

  1. Open React DevTools in your browser
  2. Go to the “Components” tab
  3. Components optimized by the compiler show a “Memo” badge
  4. The Profiler tab shows fewer re-renders

You can also check at build time - compiled components have generated memoization code in the output. Look for $ prefixed variables (the compiler’s internal naming convention) in your build output.

Migration Path

  1. Enable the ESLint plugin first. Fix all violations. This is the hardest step.
  2. Enable the compiler on a subset of your code using the sources option.
  3. Test thoroughly. The compiler should not change behavior, but verify.
  4. Enable for the entire codebase. Remove manual useMemo/useCallback/React.memo calls.
  5. Keep the ESLint plugin on. It prevents regressions as new code is written.

Next: Forms and Actions for React 19’s form handling