Next.js 23 ▲ React Compiler Support — Automatic memoization integration
The React Compiler is a build-time tool that automatically memoizes React components and hooks. It analyzes the data flow and mutability of the code and inserts the equivalent of useMemo, useCallback, and React.memo where it can prove that the optimization does not change behavior . Next.js 16 promoted the integration from experimental to stable, and the reactCompiler option is now a top-level configuration key . It is not enabled by default. The Next.js team is still gathering build-performance data, and the compiler relies on Babel, which makes the build slower than the default Rust-based path .
This chapter covers the reactCompiler option, the dependency that must be installed, the opt-in and opt-out annotations, the Next.js SWC optimization that limits the compiler to relevant files, the experimental Rust port that removes the Babel dependency, and the Rules of React that the compiler requires the code to follow.
Key point: Enabling reactCompiler: true requires babel-plugin-react-compiler as a dev dependency. Next.js applies the compiler only to files that contain JSX or React Hooks, which keeps the build cost localized. The compiler is safe to enable on a codebase that follows the Rules of React, but existing manual memoization should be left in place until the compiler’s output is tested.
Why the integration is stable but not default
The Babel dependency problem. The React Compiler runs through a Babel plugin. Babel is a JavaScript-based transform, and it is slower than the SWC-based transforms that Next.js uses for everything else. The Next.js documentation states that enabling the compiler makes development and build times higher because of the Babel dependency . The integration is stable, but it is not free, and the default is off so that projects that do not need the compiler do not pay the cost.
The SWC optimization. Next.js does not run the Babel plugin on every file. It uses a custom SWC optimization to analyze the project and identify the files that contain JSX or React Hooks. Only those files are passed to the React Compiler. The documentation describes this as the mechanism that “avoids compiling everything and keeps the performance cost minimal” . A project with a large JavaScript codebase and a small React surface pays for the compiler only on the React files.
The Rust port problem. Next.js 16.3.0 introduced an experimental turbopackRustReactCompiler option. When enabled, the compiler runs as native Rust code inside Turbopack instead of through Node.js and Babel. The documentation states that this “typically results in a noticeable performance improvement” and that it removes the need to install babel-plugin-react-compiler . The option is experimental and supported only with Turbopack, but it is the direction that the integration is moving.
a. The installation and the configuration
The compiler requires two changes: the dev dependency and the configuration key.
# Install the Babel plugin
npm install --save-dev babel-plugin-react-compiler
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
reactCompiler: true,
}
export default nextConfig
The reactCompiler option was moved from experimental.reactCompiler to the top-level key in Next.js 16 . A project that is upgrading from Next.js 15 must move the setting, because the experimental key is no longer the supported path.
The verification is the React DevTools. A component that the compiler has optimized shows a “Memo ✨” badge next to its name in the Components tab . A component that is not optimized does not show the badge, which is the signal that the compiler either skipped the file or found a pattern it could not analyze.
b. The annotations and the compilation modes
The compiler has two modes. The default infer mode compiles every component and hook that matches its analysis. The annotation mode compiles only the components and hooks that are marked with the "use memo" directive .
// next.config.ts — opt-in mode
const nextConfig: NextConfig = {
reactCompiler: {
compilationMode: 'annotation',
},
}
// A component that opts in
export default function Page() {
'use memo'
return <div>{/* ... */}</div>
}
The "use no memo" directive is the opposite: it opts a component out of the compiler even when the file is in scope . The directive is the escape hatch for a component that the compiler mis-optimizes or that relies on a pattern the compiler does not support.
The annotation mode is the safer rollout strategy. A team can enable the compiler in opt-in mode, annotate a few components, verify the behavior in production, and expand the set of annotated components over time. The infer mode is the end state, but it is a larger change because it touches every component in the project.
c. The Rules of React and the validation passes
The compiler is only safe if the code follows the Rules of React. The compiler’s validation passes encode those rules, and the diagnostics are surfaced through eslint-plugin-react-hooks . A component that violates the rules may be skipped by the compiler, which means the optimization does not apply to it, or it may be compiled incorrectly if the violation is subtle.
The rules that matter most for the compiler are the ones that the compiler’s analysis depends on. A component must be idempotent: the same props and state must produce the same output. Props and state must be treated as immutable. Side effects must run outside the render phase. Hooks must be called at the top level . A component that violates one of these rules is a candidate for a skipped optimization or a mis-compilation.
The eslint-plugin-react-hooks package includes the compiler-powered rules: set-state-in-render, set-state-in-effect, refs, and the other rules that the compiler uses to decide whether a component is safe . The linter does not require the compiler to be installed, which means a project can adopt the lint rules first and the compiler later.
d. The manual memoization policy
The compiler does not remove the need for manual memoization entirely. The React team’s guidance distinguishes new code from existing code.
For new code, the recommendation is to rely on the compiler and use useMemo or useCallback only when precise control is needed. The common case is a memoized value that is used as an effect dependency: without the manual memo, the effect may fire more often than intended .
For existing code, the recommendation is to leave the manual memoization in place or test carefully before removing it. The compiler’s output can change when the manual memo is removed, because the compiler’s analysis is not a one-to-one replacement for the developer’s intent . A project that removes every useMemo on the same day that it enables the compiler is taking a larger risk than a project that enables the compiler first and removes the manual memos later.
The reactCompiler option also accepts an environment object with enablePreserveExistingMemoizationGuarantees. The option controls whether the compiler preserves the existing manual memoization. The default depends on the compiler implementation, which is why the documentation recommends setting it explicitly .
Complete Example Session
# ============================================
# PART 1: THE DEPENDENCY
# ============================================
npm install --save-dev babel-plugin-react-compiler
// ============================================
// PART 2: THE CONFIGURATION
// ============================================
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
reactCompiler: true,
}
export default nextConfig
// ============================================
// PART 3: THE OPT-IN MODE
// ============================================
const nextConfig: NextConfig = {
reactCompiler: {
compilationMode: 'annotation',
},
}
// ============================================
// PART 4: THE OPT-IN DIRECTIVE
// ============================================
export default function Page() {
'use memo'
return <div>{/* ... */}</div>
}
// ============================================
// PART 5: THE OPT-OUT DIRECTIVE
// ============================================
export default function LegacyComponent() {
'use no memo'
return <div>{/* ... */}</div>
}
// ============================================
// PART 6: THE MANUAL MEMOIZATION ESCAPE HATCH
// ============================================
function Component({ data }) {
// The compiler memoizes this by default.
// The manual memo is kept because the value
// is used as an effect dependency.
const processed = useMemo(() => process(data), [data])
useEffect(() => { /* ... */ }, [processed])
}
// ============================================
// PART 7: THE PRESERVE GUARANTEE OPTION
// ============================================
const nextConfig: NextConfig = {
reactCompiler: {
environment: {
enablePreserveExistingMemoizationGuarantees: true,
},
},
}
// ============================================
// PART 8: THE RUST PORT
// ============================================
const nextConfig: NextConfig = {
reactCompiler: true,
experimental: {
turbopackRustReactCompiler: true,
},
}
// ============================================
// PART 9: THE REACT 18 TARGET
// ============================================
// For a project that is not yet on React 19:
const nextConfig: NextConfig = {
reactCompiler: {
target: '18',
runtimeModule: 'react-compiler-runtime',
},
}
// ============================================
// PART 10: THE DECISION
// ============================================
// New project + React 19 → infer mode
// Existing project → annotation mode first
// Turbopack + performance → Rust port
// React 18 → target 18 + runtime module
The ten parts covered the dependency, the configuration, the opt-in mode, the opt-in directive, the opt-out directive, the manual memoization escape hatch, the preserve guarantee option, the Rust port, the React 18 target, and the decision.
Quick Reference
The configuration
| Option | Value | Effect |
|---|---|---|
reactCompiler | true | Enable in infer mode |
reactCompiler.compilationMode | 'annotation' | Compile only annotated components |
reactCompiler.environment | Object | Compiler environment options |
experimental.turbopackRustReactCompiler | true | Rust port (Turbopack only) |
The directives
| Directive | Effect |
|---|---|
'use memo' | Opt in (annotation mode) |
'use no memo' | Opt out |
The dependency
| Package | Type | Required |
|---|---|---|
babel-plugin-react-compiler | devDependency | Yes (Babel path) |
react-compiler-runtime | dependency | For React 17/18 targets |
The React version targets
| React version | Configuration |
|---|---|
| React 19+ | Default (target not needed) |
| React 17/18 | target: '18' and runtimeModule: 'react-compiler-runtime' |
Best Practices
✅ Do This:
# Install the Babel plugin before enabling the option
npm install --save-dev babel-plugin-react-compiler # ✅
// Start with annotation mode on an existing codebase
reactCompiler: { compilationMode: 'annotation' } // ✅
// Opt out a component that the compiler mis-optimizes
'use no memo' // ✅
// Keep the manual memo when it is an effect dependency
const value = useMemo(() => compute(data), [data]) // ✅
❌ Don’t Do This:
// Enable the compiler without the Babel plugin
reactCompiler: true // ❌ (build fails)
// Remove every useMemo on the day the compiler is enabled
// The compiler output is not a one-to-one replacement // ❌
// Use the Rust port with webpack
experimental: { turbopackRustReactCompiler: true } // ❌ (Turbopack only)
// Assume the compiler fixes a Rules of React violation
function Component() { state.value++; return <div /> } // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
Build fails on compiler-runtime | React 18 target not configured | Add target: '18' and the runtime module |
| Component does not get the Memo badge | Rules of React violation or unsupported pattern | Check the lint diagnostics |
| Build is significantly slower | Babel dependency | Use the Rust port or accept the cost |
| Performance regression after enabling | The compiler skipped a component | Profile and add manual memoization |
| Effect fires more often | Manual memo removed | Keep the memo for effect dependencies |
Real-World Examples
1. The dependency
npm install --save-dev babel-plugin-react-compiler
2. The configuration
reactCompiler: true
3. The annotation mode
reactCompiler: { compilationMode: 'annotation' }
4. The opt-in
'use memo'
5. The opt-out
'use no memo'
6. The Rust port
experimental: { turbopackRustReactCompiler: true }
7. The React 18 target
reactCompiler: { target: '18', runtimeModule: 'react-compiler-runtime' }
8. The preserve option
reactCompiler: { environment: { enablePreserveExistingMemoizationGuarantees: true } }
9. The DevTools badge
Memo ✨ next to the component name
10. The decision
New project → infer; existing → annotation; React 18 → target 18
Visual
The compiler pipeline
┌─────────────────────────────────────────────────────────────┐
│ next build │
│ │ │
│ ▼ │
│ SWC analyzes the project │
│ │ │
│ ├── JSX or Hook? ──► Babel + React Compiler │
│ │ │
│ └── No JSX or Hook ──► skip │
│ │ │
│ ▼ │
│ Memoized output │
└─────────────────────────────────────────────────────────────┘
The two modes
┌─────────────────────────────────────────────────────────────┐
│ infer (default) │
│ → every component and hook that matches the analysis │
│ │
│ annotation │
│ → only components and hooks with 'use memo' │
│ → 'use no memo' opts out │
└─────────────────────────────────────────────────────────────┘
The manual memo policy
┌─────────────────────────────────────────────────────────────┐
│ New code → rely on the compiler │
│ Effect dependency → keep useMemo │
│ Existing memo → leave in place, test before removing │
└─────────────────────────────────────────────────────────────┘
The build cost
┌─────────────────────────────────────────────────────────────┐
│ Babel path │
│ → slower build, stable │
│ │
│ Rust port (Turbopack only) │
│ → faster build, experimental │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Stability | Stable in Next.js 16 |
| Configuration key | reactCompiler (top-level) |
| Required dependency | babel-plugin-react-compiler |
| Default mode | infer |
| Opt-in mode | compilationMode: 'annotation' |
| Opt-in directive | 'use memo' |
| Opt-out directive | 'use no memo' |
| SWC optimization | Compiler applied only to JSX/Hook files |
| Rust port | experimental.turbopackRustReactCompiler (Turbopack only) |
| React 18 support | target: '18' + react-compiler-runtime |
Key takeaways:
- The React Compiler is stable in Next.js 16, but it is not enabled by default. The
reactCompileroption is a top-level key, and enabling it requiresbabel-plugin-react-compileras a dev dependency. The compiler runs through Babel, which makes the build slower than the default Rust-based path . - Next.js limits the compiler to the files that need it. A custom SWC optimization analyzes the project and applies the compiler only to files that contain JSX or React Hooks. This keeps the build cost localized and is the reason the integration is practical on a large codebase .
- The annotation mode is the safer rollout. The
compilationMode: 'annotation'setting compiles only the components and hooks marked with'use memo'. The'use no memo'directive opts a component out. The defaultinfermode compiles every matching component, which is a larger change for an existing codebase . - The compiler requires the Rules of React. The validation passes encode the rules and surface the diagnostics through
eslint-plugin-react-hooks. A component that violates the rules may be skipped or mis-compiled. The lint rules can be adopted before the compiler is enabled . - Existing manual memoization should be left in place. The React team’s guidance is to rely on the compiler for new code and to keep the manual memos in existing code until the compiler’s output is tested. The common exception is a memoized value used as an effect dependency .
- The Rust port is the performance direction. The
experimental.turbopackRustReactCompileroption runs the compiler as native Rust code inside Turbopack, which removes the Babel dependency and improves the build speed. It is experimental and supported only with Turbopack . - The React 18 target requires the runtime module. A project that is not yet on React 19 sets
target: '18'and installsreact-compiler-runtime. The default target is React 19, and a React 18 project without the target configuration fails on thecompiler-runtimeimport .
Remember: The React Compiler is an automatic memoization tool that replaces most manual useMemo, useCallback, and React.memo calls. In Next.js 16 it is stable but not default, and enabling it costs build time because of the Babel dependency. Use the annotation mode for an existing codebase, keep the manual memos that are effect dependencies, and check the DevTools for the Memo badge to verify that the compiler is actually optimizing the component. The compiler is safe when the code follows the Rules of React, and the lint rules that enforce those rules can be adopted independently.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!