Welcome to HowToShipIt — practical how-to guides for developers: code, AI tools, and servers, explained step by step.

React Compiler: How to Enable It Without Breaking Your App (2026)

The React Compiler is the most useful thing to happen to React performance in years: a build-time tool that inserts the memoization you would otherwise write by hand with useMemo, useCallback, and React.memo. It hit its first stable release (v1.0) in October 2025, Next.js 16 ships a stable top-level toggle for it, and Vite’s current plugin has a one-line preset. Turning it on looks trivial — and it is — but most people skip the two steps that make the difference between “noticeably faster” and “quietly unchanged”. This guide walks you through enabling it in Next.js, Vite, and Expo, and shows how to prove it’s actually working.

What the React Compiler actually does

The compiler is a Babel plugin, MIT licensed, developed inside the facebook/react repository. During your build it statically analyzes each component and hook, figures out which values each expression actually depends on, and rewrites the code so expressions are recomputed only when something they read has changed. Your source code stays the same; the compiled output gets fine-grained memoization that no human would write.

For a component like this:

function Hello({ name }) {
  const onClick = () => alert(name);
  return <button onClick={onClick}>Hi</button>;
}

The compiler produces output that caches the handler and the rendered element in a small per-instance cache (imported from "react/compiler-runtime") and reuses both when name hasn’t changed. It’s memo, useMemo, and useCallback at expression granularity — without a single dependency array to get wrong.

The official React team’s guidance is pragmatic, not maximalist: for new code, rely on the compiler and reach for useMemo/useCallback only where you want precise control; for existing code, leave your existing memoization in place — removing it can change compilation output — and only strip it after careful testing.

The one failure mode you must know about: silent skip

The compiler assumes your code follows the Rules of React: pure render, immutability (no mutating props, state, or values returned by hooks during render), stable hook order, and refs read or written only inside effects and event handlers — never during render.

Here’s the catch, in the official docs’ own words: “When the compiler reports a diagnostic, it means that the compiler was able to statically detect a pattern that is not supported or breaks the Rules of React. When it detects this, it automatically skips over those components and hooks, while keeping the rest of your app compiled.”

That is a deliberately forgiving design — your build won’t fail — but it means the compiler’s failure mode is silent. You flip the toggle, your app builds, and half your components quietly aren’t compiled because of a mutation you forgot about two years ago. The whole point of this guide’s order of operations is to eliminate that surprise before you switch it on.

Step 1: Lint first (the step everyone skips)

The React team ships an ESLint rule that reports exactly the patterns the compiler will skip. The official docs state that “React Compiler diagnostics are automatically surfaced by this ESLint plugin, and can be used even if your app hasn’t adopted the compiler yet” — so you can (and should) run it in CI before you touch your build config.

One important detail, verified at publish time: the old standalone package eslint-plugin-react-compiler is stale (its npm latest is a May 2025 release candidate). The current official guidance is to use eslint-plugin-react-hooks instead:

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: { 'react-hooks/react-compiler': 'error' },
  },
];

Run eslint . and fix what it flags. The typical hits in a real codebase: mutating a prop or state value during render, reading a ref during render, conditional hook calls, and impure render logic like Math.random() or fetches directly in the component body. Fixing these makes your code better regardless of the compiler — which is why the lint-first step is worth it even if you decide to stop here.

Step 2: Flip the toggle for your framework

Next.js 16 (stable, top-level option)

In Next.js 16 the option was stabilized and moved from experimental.reactCompiler to a top-level config key:

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

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

export default nextConfig;

Install the Babel plugin as a dev dependency:

pnpm add -D babel-plugin-react-compiler

One honest caveat from the Next.js docs: the compiler runs through Babel, while Next.js otherwise uses the Rust-based SWC toolchain precisely because it’s faster. Next.js softens this with a pre-pass that detects which files actually contain JSX or hooks and only runs the compiler on those, and the official wording is that “you may still see slightly slower builds compared to the default Rust-based compiler, but the impact is small and localized.”

If you’re still on Next.js 15, the experimental form still applies:

module.exports = {
  experimental: { reactCompiler: true },
};

Vite (the v6 gotcha)

Vite setup depends on your @vitejs/plugin-react version, and this is where stale tutorials burn people. In version 6, the inline Babel option was removed, so the setup goes through a separate Babel plugin:

npm install --save-dev --save-exact babel-plugin-react-compiler@latest
npm install --save-dev @rolldown/plugin-babel
// vite.config.js
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()] })],
});

If you’re on an older @vitejs/plugin-react (below 6.0), the shorter inline form still works:

react({ babel: { plugins: ['babel-plugin-react-compiler'] } })

Expo

Expo SDK 54 enables the React Compiler in its default template. For an existing app, opt in via app.json:

{
  "expo": {
    "experiments": { "reactCompiler": true }
  }
}

Step 3: Verify it’s actually compiling

Don’t trust the toggle. Check the build output. Find one of your compiled components and look for the compiler’s fingerprint: an import from "react/compiler-runtime", exactly as shown in the React docs’ own “check build output” example:

import { c as _c } from "react/compiler-runtime";

See that import in a component’s compiled output and the compiler touched it. Don’t see it anywhere? Either your files have no JSX/hooks (unlikely for your whole app) or something is misconfigured — check your plugin order and version.

One installation detail worth knowing: if you’re on React 19, the runtime ships inside the react package itself — nothing extra to install. If you’re still on React 17 or 18, install the separate react-compiler-runtime polyfill as a real dependency:

npm install react-compiler-runtime@latest

Step 4: Roll out safely on a real codebase

On a greenfield app, reactCompiler: true everywhere is fine. On an existing app, the gradual path is safer:

Opt-in mode (Next.js). Set the compiler to annotation mode, then annotate the components you want compiled:

const nextConfig: NextConfig = {
  reactCompiler: { compilationMode: 'annotation' },
};
function Dashboard() {
  "use memo";
  // ...
}

Opt-out escape hatch. The opposite directive exists for the component that fights the compiler:

function ProblematicComponent() {
  "use no memo";
  // ...
}

Don’t rip out your memoization. The React docs explicitly recommend leaving existing useMemo, useCallback, and React.memo in place on existing code, because removing them can change what the compiler emits. Clean them up later, tested, not on day one.

Make the memoization-guarantee setting explicit. You can configure whether the compiler preserves existing memoization guarantees from useMemo/useCallback/React.memo. When omitted, the default comes from the active compiler implementation — which can differ — so the Next.js docs recommend setting it explicitly:

reactCompiler: { environment: { enablePreserveExistingMemoizationGuarantees: false } }

Five rule violations to fix before you enable it

These are the patterns the ESLint rule flags and the compiler silently skips. Fix them during the lint-first step:

  1. Mutating values during render. Pushing into an array from props or mutating an object from state while rendering — move it to an event handler or effect.
  2. Reading refs during render. ref.current in the component body is a render-time side effect; read refs only in effects and handlers.
  3. Conditional hooks. Any useState or useEffect behind an if — hoist to the top level, every render, same order.
  4. Impure render logic. Math.random(), Date.now(), or fetches directly in the component body — move them out of render.
  5. Stale mutation workarounds. Anything you’ve written that “works around” the Rules of React — the compiler won’t touch those components, and the ESLint rule will tell you which ones they are.

The pragmatic move is to get the ESLint rule to zero errors with react-compiler: 'error' in CI, then flip the build toggle. From there it’s incremental: the compiler skips only what it can’t prove safe, and each violation you fix later gets that component compiled on the next build.

When not to enable it (yet)

The compiler is stable and battle-tested at Meta scale, but it’s not free. Builds get slightly slower where Babel enters the pipeline (notably Next.js), the official SWC plugin is still in progress, and if your codebase is full of rules violations you’ll get little benefit until you fix them. If your app already runs fine on hand-written memoization and your team isn’t ready to make the lint rule a CI gate, wait — the compiler will still be there next quarter, and it’ll be faster to adopt once you’ve cleaned up the flagged violations.

Further Reading & References

Leave a Comment