React Compiler

26. July, 2025 11 min read Develop

Forget About Memoization

The React Compiler, formerly known as React Forget, is a build-time tool that automatically optimizes your React applications. It analyzes your code and inserts memoization where needed, so React only re-renders components when state actually changes — without you writing a single useMemo or useCallback.

Currently in beta and already deployed across Meta’s production applications including Facebook, Instagram, and Threads, the React Compiler represents one of the most ambitious changes to the React ecosystem in years. In this post, we’ll explore how it works, how to set it up, and what it means for the way you write React code.

The Problem It Solves

React’s rendering model is simple: when state changes, re-render the component and its children. This simplicity is one of React’s strengths, but it comes with a performance cost. Components re-render even when their output hasn’t changed, leading to unnecessary work.

The traditional solution has been manual memoization using three APIs:

  • useMemo — memoize expensive computations
  • useCallback — memoize callback functions to preserve referential equality
  • React.memo — skip re-rendering a component if its props haven’t changed

These APIs work, but they come with significant downsides. They add cognitive overhead — developers must constantly think about which values to memoize, which dependencies to track, and where to wrap components. A study at Meta found that only about 8% of React pull requests used manual memoization, yet those PRs took 31-46% longer to author. That’s a substantial productivity tax for an optimization that should be automatic.

Even experienced developers get memoization wrong. Missing a dependency in useMemo creates stale data bugs. Over-memoizing wastes memory. Under-memoizing leaves performance on the table. The React Compiler eliminates this entire category of decisions.

How It Works

The React Compiler is implemented as a Babel plugin that runs at build time. It analyzes your component code and automatically inserts memoization logic, producing optimized output without changing your source files.

Under the hood, it uses a Control Flow Graph (CFG)-based High-Level Intermediate Representation (HIR). In simpler terms, it converts your code into an internal representation that it can analyze for data flow and mutability patterns. It then determines which values can be safely cached and wraps them in a memoization layer.

Here’s what the compiler does to a simple component:

// Your source code
export default function Greeting({ name }: { name: string }) {
  const message = `Hello, ${name}!`;

  return <div>{message}</div>;
}
// Compiled output (simplified)
import { c as _c } from "react/compiler-runtime";

export default function Greeting({ name }: { name: string }) {
  const $ = _c(2);
  let t0;

  if ($[0] !== name) {
    const message = `Hello, ${name}!`;
    t0 = <div>{message}</div>;
    $[0] = name;
    $[1] = t0;
  } else {
    t0 = $[1];
  }

  return t0;
}

The compiler creates a cache array ($) that stores memoized values. On the first render, values are computed and stored. On subsequent renders, if the dependencies haven’t changed, cached values are reused. This is essentially what you’d do manually with useMemo, but the compiler does it for every value in every component, with perfect dependency tracking.

What’s remarkable is that the compiler can memoize in situations that are impossible with manual APIs. For example, it can memoize code that appears after a conditional early return — something useMemo cannot handle because hooks must be called unconditionally at the top level.

Setting Up the Compiler

Getting started with the React Compiler requires installing the Babel plugin and configuring it for your build tool.

1. Install the Plugin

npm install --save-dev --save-exact babel-plugin-react-compiler@beta

The --save-exact flag is recommended because the compiler’s memoization patterns may change between versions. Pinning ensures consistent behavior across your team.

2. Configure for Next.js

Next.js has built-in support. Add the compiler to your next.config.js:

// next.config.js
const nextConfig = {
  experimental: {
    reactCompiler: true,
  },
};

module.exports = nextConfig;

That’s it for Next.js — no Babel configuration needed. The framework handles the integration automatically.

3. Configure for Vite

For Vite projects, add the plugin to your React plugin’s 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'],
      },
    }),
  ],
});

4. Configure for Other Build Tools

For any Babel-based setup, add the plugin to your babel.config.js. The compiler plugin must run first in the pipeline, before any other transforms:

// babel.config.js
module.exports = {
  plugins: [
    'babel-plugin-react-compiler', // must be first!
    // ... other plugins
  ],
};

React Version Compatibility

The compiler works with React 19 out of the box. For React 17 and 18 projects, you need the runtime polyfill:

npm install react-compiler-runtime@beta

Then set the target version in your config:

// babel.config.js
module.exports = {
  plugins: [
    ['babel-plugin-react-compiler', { target: '18' }],
  ],
};

This makes adoption possible even if you haven’t upgraded to React 19 yet, which is great for larger projects on a gradual migration path.

The Rules of React

The compiler relies on your code following the Rules of React. These aren’t new rules — they’ve always been part of React’s contract — but the compiler enforces them more strictly because it depends on them for correctness.

Components Must Be Pure

Components should be idempotent: the same inputs must produce the same output. Side effects belong in useEffect, event handlers, or other designated escape hatches — never in the render path itself.

// Bad: side effect during render
function Counter({ count }: { count: number }) {
  document.title = `Count: ${count}`; // side effect in render!
  return <div>{count}</div>;
}

// Good: side effect in useEffect
function Counter({ count }: { count: number }) {
  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);

  return <div>{count}</div>;
}

Props and State Are Immutable

Never mutate props, state, or values that have been used in JSX. The compiler assumes immutability to determine when cached values can be reused.

// Bad: mutating state directly
function TodoList({ todos }: { todos: Todo[] }) {
  const sorted = todos.sort((a, b) => a.name.localeCompare(b.name)); // mutates!
  return <ul>{sorted.map(t => <li key={t.id}>{t.name}</li>)}</ul>;
}

// Good: create a new array
function TodoList({ todos }: { todos: Todo[] }) {
  const sorted = [...todos].sort((a, b) => a.name.localeCompare(b.name));
  return <ul>{sorted.map(t => <li key={t.id}>{t.name}</li>)}</ul>;
}

Standard Hook Rules Apply

Hooks must be called at the top level of your component and only from React functions. This is unchanged from React’s existing rules, but violations that previously caused subtle bugs can now cause incorrect memoization.

The ESLint Plugin

To catch rule violations before they become runtime bugs, install the ESLint integration. The compiler’s rules are now part of eslint-plugin-react-hooks:

npm install --save-dev eslint-plugin-react-hooks@latest
// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';

export default [
  {
    plugins: { 'react-hooks': reactHooks },
    rules: reactHooks.configs.recommended.rules,
  },
];

The React team recommends everyone install the ESLint plugin today, even if you’re not using the compiler yet. It surfaces issues like setting state during render, unsafe ref access, and other patterns that will cause problems when the compiler is eventually adopted.

Incremental Adoption

You don’t have to compile your entire codebase at once. The React Compiler supports several strategies for gradual rollout.

Directory-Based Adoption

Using Babel’s overrides, you can apply the compiler to specific directories and expand over time:

// babel.config.js
module.exports = {
  plugins: [
    // other plugins...
  ],
  overrides: [
    {
      test: ['./src/features/dashboard/**'],
      plugins: ['babel-plugin-react-compiler'],
    },
  ],
};

Annotation Mode

In annotation mode, only functions with the "use memo" directive are compiled:

// babel.config.js
module.exports = {
  plugins: [
    ['babel-plugin-react-compiler', { compilationMode: 'annotation' }],
  ],
};
function OptimizedComponent() {
  "use memo";
  // This component WILL be compiled
  return <ExpensiveTree />;
}

function RegularComponent() {
  // This component will NOT be compiled
  return <SimpleTree />;
}

This gives you precise control over which components are optimized, making it easy to test the compiler’s impact on specific parts of your application.

Opting Out

If a specific component behaves incorrectly after compilation, you can exclude it with the "use no memo" directive:

function ProblematicComponent() {
  "use no memo";
  // This component will be skipped by the compiler
  return <div>...</div>;
}

The compiler will also automatically skip functions it cannot safely optimize rather than breaking your build, so you won’t encounter build failures from incompatible code.

Real-World Performance

The results from Meta’s production deployment are compelling. On the Quest Store, the compiler delivered up to 12% improvement on initial page loads and cross-page navigations. Certain interactions became more than 2.5x faster. Memory usage remained neutral despite the performance gains.

What’s perhaps more impressive is the scale of the rollout: over 100,000 React components in Meta’s monorepo have been compiled with minimal code changes needed. This suggests that most well-written React code is already compatible with the compiler.

The performance benefits come not just from avoiding unnecessary re-renders, but from the compiler’s ability to memoize at a granularity that would be impractical to do manually. While a developer might wrap one or two expensive computations in useMemo, the compiler can memoize every intermediate value, every JSX element, and every callback — all with zero developer effort.

Debugging Compiled Components

React DevTools provides built-in support for identifying compiled components. A sparkle badge appears next to component names in the component tree, making it easy to see which components are being optimized.

If you suspect the compiler is causing a bug, the debugging workflow is straightforward:

  1. Add "use no memo" to the suspect component
  2. Test whether the bug disappears
  3. If it does, the component likely violates one of the Rules of React
  4. Check the ESLint plugin output for violations
  5. Fix the underlying issue and remove the directive

Most issues stem from code that relied on referential equality for correctness rather than just performance. For example, if an effect’s behavior depends on whether a callback is the same reference as the previous render, the compiler’s memoization can change that behavior in unexpected ways.

What About Existing Memoization?

If your codebase already uses useMemo, useCallback, and React.memo, you don’t need to remove them before adopting the compiler. Existing manual memoization will continue to work alongside the compiler’s optimizations.

However, the React team advises caution when removing existing memoization. The compiler’s output can change when manual memoization is removed, because it may choose to memoize values differently. If you want to clean up manual memoization after adopting the compiler, do it incrementally and test thoroughly.

Over time, as confidence in the compiler grows, the expectation is that new code will be written without manual memoization entirely. The compiler handles it, and the code stays clean and readable.

Limitations

While the compiler is impressive, there are some limitations to be aware of:

  • Build tool requirement: The compiler must run on original source code before any other transformations. This means it works best in projects using standard Babel or SWC pipelines.
  • Library code: Libraries must be pre-compiled by their maintainers. You cannot compile third-party dependencies in your application — the library authors need to ship compiled code themselves.
  • SWC support: The SWC integration is still experimental. Most setups currently require the Babel plugin, which can be slower than a pure SWC pipeline.
  • Beta status: As of mid-2025, the compiler is in beta. While it’s production-tested at Meta, API changes are still possible before the stable release.

Conclusion

The React Compiler is a paradigm shift in how we think about React performance. Instead of sprinkling useMemo and useCallback throughout your code and hoping you got the dependencies right, the compiler handles all of it automatically at build time — more thoroughly and more correctly than any developer could do manually.

The setup is straightforward, the incremental adoption path is well-designed, and the production results from Meta speak for themselves. Even in beta, it’s worth experimenting with in your projects. Install the ESLint plugin today to prepare your codebase, and consider enabling the compiler on a feature branch to see the impact firsthand.

‘Till next time!