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.