Enabling the Compiler and Incremental Adoption

Turning React Compiler on is rarely a single boolean in isolation. It is a build-time dependency, often mediated by your framework’s config, plus—if you are serious about keeping components optimizable—an ESLint setup that enforces the Rules of React in the same places the compiler’s validation passes look. The good news for 2026 is that the path is mainstream: greenfield templates for Next.js, Vite, and Expo frequently ship with the compiler enabled or one flag away, and the React documentation publishes an incremental adoption guide at react.dev/learn/react-compiler/incremental-adoption aimed at brownfield codebases that cannot flip a repo-wide switch overnight.

Next.js: defaults, config, and SWC

Next.js 15 began positioning the compiler as part of the default developer experience for new apps, and Next.js 16 continues that trajectory with explicit configuration. In a typical next.config.ts, enabling the compiler is intentionally boring—often a reactCompiler: true (or framework-equivalent) flag once you are on a version that expects it. Because Next bundles both Babel and SWC paths, you will see release notes calling out experimental SWC support for the compiler with faster builds on recent minors (15.3.1 and onward show up frequently in migration notes). Treat SWC as acceleration when your version supports it; treat Babel as the compatibility baseline when you need predictable behavior during an upgrade window.

From a TypeScript perspective, nothing special is required beyond a normal strict setup: the compiler runs after TS has been stripped. Where teams stumble is custom Babel plugins that transform React components in ways the compiler does not understand. If you have legacy CSS-in-JS or codegen that rewrites function bodies, plan a staging environment where you can disable those transforms selectively or run the compiler only on directories that are “plain” React.

Vite: Babel plugin in a post-Babel-default world

Vite’s default pipeline is esbuild-driven and fast precisely because it skips Babel. Opting into React Compiler means opting back into Babel for the React plugin chain, at least for the files you want optimized. Install babel-plugin-react-compiler and wire it in vite.config.ts so it runs alongside @vitejs/plugin-react (or the Babel-based variant). A minimal pattern is to enable the compiler only for src/** and exclude test utilities or Storybook entry points until they are clean.

import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

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

Exact option names drift slightly across @vitejs/plugin-react versions; the invariant is that Babel must see your component source before bundling. For library authors publishing precompiled dist code, you must run the compiler before publishing, or consumers will not get optimized components unless they recompile your sources.

Expo SDK 54 and React Native

Expo SDK 54 and newer enable the compiler by default for new applications, which matters because React Native’s performance story has always been sensitive to render churn on the JS thread. The workflow most teams want is: create a fresh app with npx create-expo-app@latest, confirm the template’s Metro and Babel config, then diff that against an older project during migration. Native modules and bridge-heavy screens still need profiling; the compiler reduces redundant React work, not layout thrash or heavy image decoding.

Runtime package for React before 19

The plugin targets multiple React majors. On React 19, the runtime includes what the emitted code expects. On React 17 and 18, you add react-compiler-runtime so the transformed components have the correct supporting hooks and caches. Skipping that dependency produces mystifying runtime errors that look like “invalid hook call” or missing internals—exactly the class of issue that gives build-time tools a bad name. Your package.json should list react, react-dom, the compiler plugin, and, when applicable, react-compiler-runtime with versions that match the matrix in the compiler’s release notes.

ESLint as the adoption gate

Automatic memoization is only trustworthy when components obey deterministic render semantics. The recommended preset for eslint-plugin-react-hooks ships rules aligned with compiler validation: patterns like set-state-in-render, unsafe ref access during render, and effect bodies that accidentally recreate the problems renders must avoid. Install the latest plugin and extend the recommended config in eslint.config.js (flat config) or .eslintrc (legacy). Fixing lint errors in a folder is often a good “definition of done” before you enable compilation for that folder.

// eslint.config.js (illustrative)
import reactHooks from "eslint-plugin-react-hooks";

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

Pair that with TypeScript’s own checks: noImplicitReturns, exhaustive switches on discriminated props, and avoiding any on values that flow to JSX all reduce the chance that analysis bails out silently.

Incremental rollout strategies

Few mature codebases pass every rule on day one. A practical rollout mirrors feature flags: enable the compiler per package in a monorepo, per route in a Next.js app by splitting server and client boundaries cleanly, or per directory in Vite via include/exclude globs. Start with leaf components—presentational panels, list items, form controls—where data flow is simple and side effects live in event handlers or effects. Defer layout roots, legacy class components, and anything using higher-order patterns that obscure the component name from static analysis.

For each stage, measure: React DevTools profiler before and after, plus real navigation metrics if you have RUM. Meta’s public numbers (for example Quest Store improvements on the order of twelve percent for some load and navigation scenarios, and multiplicative gains on specific interactions) set expectations that user-visible wins track redundant render reduction, not just fewer useMemo calls in source.

Version pinning and upgrades

Compiler output is allowed to change between versions as heuristics improve. If your test suite is mostly integration-free or snapshot-heavy, prefer npm install --save-exact babel-plugin-react-compiler@1.0.0 (substitute your chosen version) so upgrades are deliberate. When you do upgrade, rerun profiling on critical paths: a new version might memoize more aggressively and surface a latent mutation bug that previously “worked” because excessive re-rendering masked stale UI.

Greenfield starters (npx create-next-app@latest, npm create vite@latest, npx create-expo-app@latest) increasingly scaffold compiler-ready config. For existing apps, treat enablement as a toolchain migration: align React major, add runtime if needed, wire the plugin, fix lint violations, then delete redundant manual memoization only where profiling proves it is safe.

The compiler does not remove the need for engineering judgment; it removes the default need to manually thread referential stability through every layer. With the build wired and ESLint enforcing the same contracts the analyzer assumes, you can adopt incrementally without betting the release on a single big bang—and the next section makes explicit what those contracts are, and where manual hooks still earn their keep.