Skip to content

React Compiler and Automatic Memoization in Practice

The React Compiler auto-inserts memoization at build time. How to install it, verify it works, and understand the cases it skips.

· · 10 min read

Updated: October 9, 2026

Code editor showing React component optimization with memoization patterns

Quick Take

The React Compiler auto-inserts useMemo and useCallback at build time, eliminating manual memoization for most components. It silently skips components that violate Rules of Hooks, learn how to verify it's actually optimizing your code before shipping.

The React Compiler is a build-time Babel plugin that reads your component code, figures out which values can be safely cached, and inserts the memoization for you. No component rewrites. No manual dependency arrays. You configure it once and it runs on every build.

Once it's on, plenty of manual useCallback and useMemo calls turn out to be removable, not because the compiler requires it, but because they're redundant once its coverage is verified through DevTools. That's the right rollout pace: enable, verify, clean up incrementally. I won't quote a coverage percentage here, because any figure I gave would be a guess about your codebase.

What Does the React Compiler Actually Do?

It's a static analysis pass. The compiler reads your component code, determines which computations and returned functions can be memoized without changing behavior, and rewrites the output to include those memo calls with correct dependency arrays.

// What you write
function ProductCard({ product, onAddToCart }) {
  const price = formatPrice(product.price, product.currency);
  const handleClick = () => onAddToCart(product.id);

  return (
    <div onClick={handleClick}>
      <h3>{product.name}</h3>
      <p>{price}</p>
    </div>
  );
}

// What the compiler generates (roughly)
function ProductCard({ product, onAddToCart }) {
  const price = useMemo(
    () => formatPrice(product.price, product.currency),
    [product.price, product.currency]
  );
  const handleClick = useCallback(
    () => onAddToCart(product.id),
    [onAddToCart, product.id]
  );

  return (
    <div onClick={handleClick}>
      <h3>{product.name}</h3>
      <p>{price}</p>
    </div>
  );
}

The dependency arrays are the part that's easy to get wrong by hand, and wrong dep arrays are where subtle stale-closure bugs live. The compiler gets them right because it has full visibility into what each function captures.

Dependency arrays are the part that's easy to get wrong by hand, and wrong dep arrays are where subtle stale-closure bugs live. For a refresher on why dep arrays matter, see .

Share this Post on X Bluesky
A close-up of interlocking brass clockwork gears
Photo by Curated Lifestyle on Unsplash

How Do You Install the React Compiler?

npm install -D --save-exact babel-plugin-react-compiler@latest
npm install -D eslint-plugin-react-hooks@latest

The compiler's lint rules used to ship as a separate package, eslint-plugin-react-compiler. Since the 1.0 release they live in eslint-plugin-react-hooks, and the React team's announcement says you can remove the old package if you have it.

Next.js 16 and later:

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

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

export default nextConfig;

Next.js 16.0.0 stabilized the option and moved it out of experimental. You still install babel-plugin-react-compiler yourself. Next.js applies it through a custom SWC step that only touches files with JSX or hooks, so builds stay faster than running the Babel plugin on everything.

Next.js 15:

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

export default nextConfig;

Vite with @vitejs/plugin-react 6 and later:

// vite.config.ts
import { defineConfig } from 'vite';
import react, { reactCompilerPreset } from '@vitejs/plugin-react';
import babel from '@rolldown/plugin-babel';

export default defineConfig({
  plugins: [
    react(),
    babel({ presets: [reactCompilerPreset()] }),
  ],
});

That setup needs @rolldown/plugin-babel as an extra dev dependency. On older plugin-react versions, the earlier form still applies: pass babel: { plugins: ['babel-plugin-react-compiler'] } to react().

Plain Babel:

{
  "plugins": ["babel-plugin-react-compiler"]
}

Add the ESLint plugin immediately, it catches patterns the compiler will refuse to optimize before you hit a runtime warning. Static analysis is the technique the compiler and its ESLint companion both use, examining your source code without running it to determine which patterns are safe to memoize. Here's the order that avoids surprises:

  1. Install babel-plugin-react-compiler and the latest eslint-plugin-react-hooks together
  2. Turn on the plugin's recommended preset and make CI fail on what it reports, so violations can't slip in as warnings nobody reads
  3. Fix every violation the linter reports before enabling the compiler in your build
  4. Enable the compiler in your framework config and rebuild
  5. Check React DevTools for the sparkle badge to confirm actual coverage
// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';

export default defineConfig([
  reactHooks.configs.flat.recommended,
]);

The recommended preset carries the compiler-powered rules next to rules-of-hooks and exhaustive-deps. Among them are immutability, purity, refs, set-state-in-render and preserve-manual-memoization. If you're still on an eslintrc file, the equivalent is "extends": ["plugin:react-hooks/recommended"].

Don't skip the ESLint step. It surfaces the components you'll need to clean up before the compiler can touch them. I'd go further and run the rule alone for a full sprint before flipping the compiler on, and I wouldn't apologize for the delay: the lint pass is the cheap half of the experiment, and it names the components the compiler would otherwise skip without a word.

Installing the React Compiler takes two npm packages, babel-plugin-react-compiler and eslint-plugin-react-hooks, plus one config change per framework: a reactCompiler flag in next.config.ts for Next.js 16 (experimental.reactCompiler on 15), a Babel preset or plugin entry for Vite, or a plugin entry in a plain Babel config for everything else, no build tooling swap required.

Share this Post on X Bluesky

How Do You Verify It's Working?

React DevTools shows a sparkle badge next to components the compiler has optimized, and React's own debugging guide uses that badge as its check. Open the Components panel after a development build and you'll see coverage without any extra tooling. Reading generated build output is the harder route, since compiled code is intentionally awkward to eyeball. Coverage varies a lot by codebase, so I'd trust the badges you count in your own DevTools over any percentage in a guide, this one included.

For build-time detail, the logger option takes an object, not a string. A logger: "console" shortcut isn't in the options reference, and an older version of this guide used it. The documented shape is a logEvent function:

// babel.config.js
module.exports = {
  plugins: [
    [
      'babel-plugin-react-compiler',
      {
        logger: {
          logEvent(filename, event) {
            if (event.kind === 'CompileSuccess' || event.kind === 'CompileError') {
              console.log(event.kind, filename);
            }
          },
        },
      },
    ],
  ],
};

The reference lists CompileSuccess, CompileError, CompileDiagnostic, CompileSkip, PipelineError and Timing as event kinds. Filtering to the first two gives a first-pass audit of what compiled and what got skipped. It's noisy on a big codebase, so run it once rather than leaving it on. The React Compiler Playground lets you paste a component and see the compiled output before enabling it in your project, useful for understanding what the compiler will do to a specific pattern.

A black-and-white close-up of a large gear meshing with a chain drive
Photo by Mike Hindle on Unsplash

How Does Installation Differ by Framework?

FrameworkSetupConfig location
Next.js 16Native support, stabletop-level reactCompiler in next.config.ts
Next.js 15Native support, experimentalexperimental.reactCompiler in next.config.ts
Vite (plugin-react 6+)Babel preset via @rolldown/plugin-babelreactCompilerPreset() in vite.config.ts
Vite (older plugin-react)Via @vitejs/plugin-reactbabel.plugins array in vite.config.ts
ExpoSDK 54 and later configure Babel for youexperiments.reactCompiler in app.json
RsbuildVia @rsbuild/plugin-reactreactCompiler option in rsbuild.config.ts
Plain BabelDirect pluginBabel config plugins array, compiler first

What Will the Compiler Skip?

The compiler skips components that violate React's rules. Common patterns that block optimization:

Mutating props or state during render:

// Skipped, mutates the array during render
function BadList({ items }) {
  items.push({ id: 'temp' }); // compiler bails on the whole component
  return <ul>{items.map(item => <li key={item.id}>{item.text}</li>)}</ul>;
}

Conditional hook calls:

// Skipped, hook inside a condition
function ConditionalHook({ isLoggedIn, userId }) {
  if (isLoggedIn) {
    const data = useUserData(userId); // whole component is skipped
  }
}

Reading mutable external state without a subscription:

// Skipped, reads window.someGlobal which changes outside React's control
function GlobalReader() {
  return <div>{window.someGlobal}</div>;
}

The compiler doesn't crash on these, it emits a warning and moves on to the next component. The ESLint rules catch most of them at lint time before they reach the compiler.

The React Compiler skips components that mutate props during render, call hooks conditionally, or read mutable external state without a subscription, emitting a warning instead of crashing, and the compiler-powered ESLint rules catch most of these violations before the compiler even runs.

Share this Post on X Bluesky

Should You Remove Existing useMemo and useCallback?

Not all at once, and not without looking first. The compiler recognizes correct manual memoization and doesn't double-wrap it. But manual memoization written with incorrect dep arrays can interfere with the compiler's analysis.

The sane rollout: enable the compiler, check DevTools for the sparkle badges, then remove manual calls component by component where the compiler already has coverage. You'll occasionally find a manual dep array that was wrong, the compiler's correct version handles it, and you can see the difference in behavior.

For components the compiler skipped, fix the ESLint violations first. The fix is usually straightforward: move mutations out of render, un-conditionalize hook calls. Once the component is clean, the compiler handles the rest.

Don't try to fix everything at once. That incremental approach works better than a single large PR.

I only trust a gate after I've watched it fail once. In the monorepo I work in, the type-check is one tsc --noEmit -p per project, because a bare tsc --noEmit at a root with "files": [] and project references exits 0 having checked nothing. Apply the same standard here: break a component on purpose, the way BadList does above, and confirm the lint rule complains before you believe it's guarding anything.

The compiler recognizes correctly written manual memoization and will not double-wrap it, so the safe rollout order is enable the compiler first, verify coverage through the DevTools sparkle badge, then remove redundant useMemo and useCallback calls component by component rather than in one large pull request.

Share this Post on X Bluesky

Where Does the Compiler Stand Now?

The React Compiler hit its 1.0 release in October 2025, so it's no longer the RC-flagged tool this guide originally described. As of this update, 1.0.0 is still the latest tag of babel-plugin-react-compiler on npm. That matters for anyone who held off. The one set of numbers worth repeating comes from the React team's own 1.0 announcement: at Meta, initial loads and cross-page navigations improved by up to 12%, and certain interactions ran more than 2.5x faster. Those are Meta's apps, not yours, and "up to" is doing real work in that sentence.

Practically, this changes the calculus for new projects. Eighteen months ago the sane advice was "try it on a low-risk app first." Now, for any React codebase started from scratch, skipping the compiler is closer to skipping TypeScript, technically fine, but you're leaving free correctness and performance on the table for no real reason. If you're still running an app without it, the migration guide above hasn't changed, install the plugin, run the ESLint rule, verify coverage in DevTools, clean up incrementally.

React Compiler 1.0 was announced on October 7, 2025, and it's compatible with React 17 and up. According to the , at Meta initial loads and cross-page navigations improved by up to 12%, while certain interactions were more than 2.5x faster.

Share this Post on X Bluesky

Summary

The React Compiler (babel-plugin-react-compiler) automates memoization at build time, correctly placed useMemo, useCallback, and React.memo across your codebase without manual dep arrays. Install it, add the ESLint plugin, enable it in your framework config, and check React DevTools to see coverage. It skips components that violate React's rules and warns you exactly which ones. Remove manual memoization incrementally after verifying DevTools coverage. The compiler works on React 17+, you don't need to be on React 19 first. For a full picture of what React 19 itself added, see our React 19 new features guide.

Frequently Asked Questions

Does the React Compiler ship with React 19?
No. The React Compiler is a separate Babel plugin, babel-plugin-react-compiler. You install it independently and configure it in your Babel or framework config. It works with React 17 and 18 as well, not just React 19.
Should I remove useMemo and useCallback after enabling the compiler?
Not all at once. The compiler leaves correct manual memoization in place and doesn't double-wrap it. Remove manual calls incrementally, component by component, after verifying through React DevTools that the compiler is already handling those components. Don't mass-delete, inspect first.
What code will the React Compiler not optimize?
The compiler skips components that violate React's rules: mutations of props or state during render, conditional hook calls, non-idempotent render functions, and reading mutable external state without a subscription. It warns you and skips those components rather than produce incorrect output.
How do I verify the React Compiler is working?
React DevTools shows a sparkle badge next to components the compiler optimized, and React's debugging guide uses that badge as the check. For build-time detail, pass a logger object with a logEvent function in the babel-plugin-react-compiler options. It receives CompileSuccess and CompileError events per function.
Does the React Compiler work with Next.js?
Yes. Since Next.js 16 the option is stable and top-level: reactCompiler: true in next.config.ts. Next.js 15 reads it from experimental.reactCompiler. Either way you install babel-plugin-react-compiler yourself. For Vite, use a Babel preset or the plugin-react babel option, depending on the plugin version. For plain Babel, add the plugin to your Babel config.