A counter updates in a parent component. A product list below it receives the same search query as before, yet React may still call the list component again. React Compiler can avoid some of that repeated work without requiring you to wrap everything in memo, useMemo, or useCallback.
The key word is some. React Compiler is a build-time optimizer, not a new rendering engine or a guarantee that every state update will be cheap. It automatically memoizes eligible React components, hooks, values, and JSX using its analysis of data flow and the Rules of React. It can reuse unchanged calculations and child elements on later renders. It does not prevent legitimate updates when the data a component depends on changes.
The compiler reached its first stable release on October 7, 2025. It remains optional to adopt. Here is how to decide what it can help with, verify that it is actually enabled, and recognize its boundaries.
What changes when a component is compiled?
Without memoization, a parent re-render often leads React to evaluate its children even when their relevant inputs have not changed. Developers have traditionally used three related tools:
memocan skip a component render when its props compare equal.useMemocan preserve a computed value between renders while its dependencies stay the same.useCallbackcan preserve a function reference between renders while its dependencies stay the same.
These tools are useful, but their effectiveness depends on the details. Passing a fresh object or inline callback to a memoized child can defeat a manually written optimization. Conversely, memoizing an inexpensive calculation can add complexity without solving a measurable problem.
React Compiler analyzes supported components and hooks before the browser runs them. Its generated code can cache values and JSX and reuse them when their relevant inputs have not changed. That is not literally the same as inserting those three public APIs everywhere. You might see cache machinery imported from react/compiler-runtime in compiled output, but the generated form is an implementation detail, not code to copy into your application.
The official Compiler introduction describes two primary targets: avoiding cascading re-renders of unchanged child work and avoiding unnecessary repeated calculations during rendering.
This is also a narrower claim than "React becomes signal-based." Compiler memoization works within React's component model. A signal system may track which consumers read a reactive value. The two can reduce needless work through different mechanisms, as discussed in the signals and React reactivity comparison.
A small example to inspect
This is an illustrative React 19 client component for an App.jsx file. It requires a React application with React Compiler enabled to observe compiled behavior. No performance numbers or runtime results are asserted here.
import { useState } from 'react';
const PRODUCTS = [
{ id: 'notebook', name: 'Paper notebook' },
{ id: 'keyboard', name: 'Mechanical keyboard' },
{ id: 'cable', name: 'USB-C cable' },
];
function Results({ query }) {
const normalized = query.trim().toLowerCase();
const matches = PRODUCTS.filter(product =>
product.name.toLowerCase().includes(normalized)
);
return (
<ul aria-label="Matching products">
{matches.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
export default function App() {
const [query, setQuery] = useState('');
const [count, setCount] = useState(0);
return (
<main>
<label htmlFor="product-query">Filter products</label>
<input
id="product-query"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="button" onClick={() => setCount(value => value + 1)}>
Unrelated clicks: {count}
</button>
<Results query={query} />
</main>
);
}
The two interactions make a useful diagnostic pair. Clicking Unrelated clicks changes count but not query. Typing in the input changes query, so the list's filter actually needs to be reconsidered.
| Interaction | Relevant input to Results | What to look for with the compiler |
|---|---|---|
| Click the counter | Unchanged query | Eligible JSX and child work may be reused rather than recalculated |
| Type a different query | Changed query | The matching list must reflect the new input |
The sample deliberately uses only three products. It isolates the dependency question rather than constructing a synthetic slow application. A skipped component call is not the same thing as a shorter user-visible interaction, and a changed filter is not evidence that React mutated the DOM for every list item.
For the underlying distinction between render, commit, and paint, see the React rendering and Fiber guide.
Verify compilation before interpreting the results
First make sure the build actually uses the compiler. With React Developer Tools, optimized components show a Memo ✨ indicator. You can also inspect the compiled build for generated memoization logic.
Then use the React DevTools Profiler to compare the counter interaction with the query interaction. Check which components participated in commits. For trustworthy performance work, repeat the same user interactions in a production build with profiling tooling appropriate to your setup. React Strict Mode can cause additional development-only render calls, so console logging inside a component is not a reliable benchmark.
A reasonable investigation records the before-and-after behavior and checks the application's correctness, not just the presence of the Memo badge. The badge establishes that compilation occurred; it does not establish a speedup.
Enable React Compiler in an existing project
The compiler does not run simply because an app uses React 19. Your build tooling must integrate it. The official installation guide provides current setup paths for Babel, Vite, Metro, and other supported tools.
Next.js configuration depends on both the Next.js major version and the React runtime version. Install the compiler plugin as a development dependency:
npm install --save-dev --save-exact babel-plugin-react-compiler@latest
For Next.js 16 with React 19, the stable top-level setting is:
// next.config.ts (Next.js 16, React 19)
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactCompiler: true,
};
export default nextConfig;
For Next.js 15, React Compiler support is under experimental.reactCompiler, not the Next.js 16 top-level option. This distinction is confirmed by the Next.js 15 release documentation and the Next.js 16 upgrade guide. For a Next.js 15 application using React 19, the relevant setting is experimental: { reactCompiler: true }. Next.js 15.3.1 and later also have SWC-assisted file selection for the Babel-based compiler.
React 17 and React 18 require additional compatibility work. The compiler's default target is '19'. For React 18, install react-compiler-runtime@latest as a production dependency and pass target: '18' to the compiler; React 17 similarly uses target: '17'. See the official target reference. For example, the portfolio repository currently declares Next.js 15.5.28 and React 18.3.1, so neither the Next.js 16 config above nor the React 19 default is a drop-in configuration for that stack. The correct Next.js 15 / React 18 integration must be validated against the project's actual compiler plugin and Next.js types before enabling it.
The install command above pins the resolved plugin version instead of accepting a semver-range upgrade; record it in your lockfile.
If the codebase is large, incremental adoption is safer than an unobserved all-at-once rollout. One option is compilationMode: 'annotation', which compiles explicitly marked components and hooks using the "use memo" directive. Directory-scoped compilation and feature gating are other documented approaches. Validate the chosen mode in your actual build rather than assuming every file is compiled.
When the compiler cannot safely optimize your code
Automatic memoization depends on React's correctness rules. Components and hooks are expected to be pure during rendering; props and state are immutable snapshots, not values to modify in place.
For example, this pattern mutates a prop during render:
function SortedNames({ names }) {
names.sort(); // Mutates the incoming array.
return <p>{names.join(', ')}</p>;
}
The problem exists even without the compiler. Rendering the component can change an array owned by its parent, making the result depend on call order and shared mutable state. An immutable alternative is:
function SortedNames({ names }) {
const sorted = [...names].sort();
return <p>{sorted.join(', ')}</p>;
}
This change makes the render function safer to analyze. It does not prove that a particular compiler version will optimize this exact component.
Use the rules in eslint-plugin-react-hooks to catch unsupported patterns and compiler diagnostics. The compiler is designed to skip components or hooks it cannot safely optimize while continuing to handle other eligible code. A linter warning does not automatically mean the whole application fails to build.
A separate limit is shared work: Compiler memoization in one component does not create a global cache for calculations across unrelated components. If several components independently run the same expensive transformation, investigate state placement, data fetching, shared caching, or moving the work outside rendering before assuming the compiler will deduplicate it.
What if a compiled component behaves differently?
Start by reproducing the incorrect behavior, not by deleting every useMemo you find. Follow the React Compiler debugging guide:
- Check the compiler and React versions and look for Rules of React lint violations.
- Isolate the affected component or hook and the interaction that breaks.
- Temporarily opt that function out of compilation using the
"use no memo"directive. - Compare the same interaction with and without compilation. If the issue disappears, inspect mutation, hidden side effects, unstable dependencies, and memoization used for correctness.
- Fix the underlying code, remove the temporary opt-out, and repeat the test.
An opt-out looks like this:
function ProblematicPanel({ items }) {
'use no memo';
// Existing component body goes here.
return <p>{items.length} items</p>;
}
The opt-out is a diagnostic tool, not a permanent fix for incorrect component logic. A runtime difference can indicate a violation of React's rules that previously went unnoticed, but a minimal reproduction is still needed before filing a compiler bug.
Do not bulk-remove existing useMemo, useCallback, or memo wrappers during adoption. React's current guidance is to keep existing memoization unless you deliberately test its removal. These APIs still provide control when reference stability has observable meaning, including certain Effect dependencies.
How to decide whether it helped
Treat compiler adoption like any other performance change. Define the interaction, preserve a baseline, verify the optimized build, and then compare what matters to the user.
| Check | Why it matters |
|---|---|
| Compiler coverage and lint diagnostics | Confirms which code can be optimized and which code is skipped |
| Interactive correctness and Effect behavior | Catches bugs that become visible when references are reused differently |
| React Profiler commits and render cost | Shows whether repeated React work actually decreased |
| Browser performance trace | Distinguishes React work from scripting, layout, paint, and network bottlenecks |
| Bundle size and build cost | Detects tradeoffs outside render performance |
React Compiler primarily targets update performance. It cannot repair an expensive network request, a blocking event handler, a layout thrash, or an unnecessarily large download. And it does not make an impure component correct.
The useful new default is not "never think about memoization." It is write correct, straightforward React first; let the compiler handle eligible repeated work; profile when the app is actually slow. That replaces guesswork with a smaller, testable optimization problem.
