Adopting React Compiler Without Breaking Production
React Compiler passed its first anniversary as a stable release in October 2025. React's own 1.0 announcement, published October 7, 2025, called it "production-ready for both React and React Native," and the framework support has caught up fast since: Next.js exposes it as a single reactCompiler: true flag, and Expo's SDK 55 turns its lint rules on by default. The pitch is simple: stop writing useMemo, useCallback, and React.memo by hand, and let the compiler figure out what needs memoizing.
The flag itself takes thirty seconds to add. What actually takes the time, and what determines whether turning it on is a non-event or a production incident, is everything you do before and after flipping it.
What the compiler is actually doing
React re-renders a component whenever its parent re-renders, by default, whether or not anything the component reads actually changed. Manual memoization has always been the fix: wrap a value in useMemo, wrap a function in useCallback, wrap a component in React.memo, and React skips the recompute or re-render when the inputs are referentially equal to last time.
React Compiler does the same job at build time instead of by hand. It statically analyzes your component and hook functions, figures out which values and JSX trees are safe to cache, and rewrites the code to cache them automatically, no manual wrapping involved. The output isn't a drop-in replacement for your source so much as a build step: your original function in, a version instrumented with its own memoization cache out.
That only works, though, if the compiler can prove a component follows React's actual rules. When it can't prove that, it does the safe thing and leaves the component alone.
Lint first, flag second
This is the step most adoption guides bury and most incidents trace back to: install the compiler's ESLint integration before you ever add the build plugin.
npm install --save-dev eslint-plugin-react-compiler
The compiler's diagnostics are also surfaced through the standard React Hooks ESLint plugin now, so if your project already runs that, you may already be seeing some of these warnings without a separate install. Either way, run lint across the whole codebase before touching the build config. The compiler's actual behavior when it hits code it can't safely transform isn't to throw a build error. It skips that component or hook silently and keeps compiling the rest of your app. A codebase with unaddressed Rules of React violations doesn't fail loudly when you enable the compiler. It just gets a fraction of the benefit you expected, and nothing in your build output tells you which fraction.
Linting first turns that silent gap into a visible list you can work through before the flag is even on.
What actually trips the compiler up
The violations the lint rule catches are almost always the same handful of patterns, all of them things React already asked you not to do, just not enforced this strictly before:
- Conditional hook calls. A
useStateoruseEffectbehind anifor inside a loop breaks the compiler's ability to reason about render order statically, the same reason it's always broken React's own hook rules. - Refs read or mutated during render.
ref.currentis meant to be read in effects and event handlers, not render. The compiler can't treat a value as stable if it might change between when it's read and when the render actually commits. - Direct mutation of props or state. Pushing onto an array prop or reassigning a field on a state object instead of going through its setter looks fine at runtime in a lot of cases, but it breaks the referential-equality assumption the whole compiler is built on.
None of these are new rules. They're the Rules of React that have always technically applied, just not always enforced, and not always obviously broken in code that otherwise worked. The compiler is the first tool that actually depends on you following them.
Framework-specific setup gotchas
The flag itself differs slightly by framework, and each has its own order-of-operations trap:
Next.js takes a single top-level config flag after installing babel-plugin-react-compiler:
// next.config.ts
const nextConfig = {
reactCompiler: true,
};
If your Next.js version still has it under experimental.reactCompiler, check your version's docs before copying a snippet from elsewhere, since the option moved to the top level as it stabilized.
Vite, using @vitejs/plugin-react, has a plugin-ordering trap that's easy to miss: the Babel plugin needs to run before the React plugin's own JSX transform, not after. Get the order backwards and the build succeeds, the app runs, and the compiler simply never runs, silently, with no error.
Expo bundles the compiler's lint rules into eslint-config-expo as of SDK 55, so if you're on that version or later, you may not need to install anything separately. Expo also ships a healthcheck command worth running before enabling the flag on an existing app:
npx react-compiler-healthcheck@latest
Confirming it actually worked
Once the flag is on, don't assume coverage. Confirm it. React DevTools 5.0 and later marks every component the compiler successfully optimized with a small "Memo ✨" badge next to its name in the component tree, a different marker than the one shown for a component you wrapped in memo() by hand. No badge means the compiler skipped that component, which sends you back to the lint output to find out why.
This is also the point to pull up the Profiler and compare re-render counts before and after on the screens that actually matter to your app, rather than trusting a general performance claim from somewhere else. Published percentage improvements for React Compiler vary widely across blog posts and aren't independently sourced, so they're not a substitute for measuring your own components.
The order that keeps this safe
Pulling it together, the rollout order that avoids a silent regression looks like this:
- Install the ESLint integration and run it across the whole codebase, before touching any build config.
- Fix the Rules of React violations it surfaces, or deliberately opt a component out with a
"use no memo"directive if you're not ready to fix it yet. - Enable the compiler via your framework's flag.
- Confirm coverage with the Memo ✨ badge in React DevTools, and profile the screens you actually care about.
- Only then, in a separate follow-up change, start removing the manual
useMemo,useCallback, andmemo()calls the compiler has made redundant.
That last step matters more than it looks. Deleting old memoization in the same change that enables the compiler makes it much harder to tell, if something breaks, whether the cause was the compiler's own behavior or the removal of a memoized reference something else in your codebase was quietly depending on, an effect dependency array included. Separating "enable and verify" from "clean up the old code" gives you a rollback path for each step independently.