React Compiler with Next.js 16: Setup, Auto-Memoization, and What to Delete from Your Code (2026)
React Compiler lands in Next.js 16 as a stable, one-flag setup. Here's how to enable it, what useMemo and useCallback calls you can finally delete, and how to verify the wins in React DevTools.
React Compiler in Next.js 16 is an opt-in build-time optimization that automatically memoizes components, hooks, and values, replacing most hand-written useMemo, useCallback, and React.memo calls with code the compiler emits for you. Enable it with one flag in next.config.ts, install the Babel and ESLint plugins, and the compiler analyzes every component that follows the Rules of React and rewrites it to skip rerenders that produce identical output. In my profiling traces on real apps, the wins concentrate in lists, dashboards, and form-heavy screens (exactly the places you used to wrap everything in useMemo and still saw jank).
React Compiler reached stable in React 19.1 and ships as babel-plugin-react-compiler v1.x. Next.js 16 wires it in with experimental.reactCompiler: true.
The compiler only touches components and hooks that follow the Rules of React; opt a file out with the "use no memo" directive when needed.
You can delete almost all defensive useMemo/useCallback/React.memo calls once it's enabled. Keep them only for non-React caches and expensive idempotent computations.
It composes with Server Components: the compiler only runs on Client Components (anything reachable from 'use client'), and it ignores server modules.
Install eslint-plugin-react-compiler first. Its warnings tell you which components the compiler is silently skipping because they break the Rules of React.
Expect a one-time 5–15% bundle-size increase and 10–40% interaction-latency improvement on highly interactive screens; static pages see almost no change.
What is React Compiler and what does it do?
React Compiler is a build-time tool, distributed as babel-plugin-react-compiler, that reads your Client Component source and emits an equivalent version where re-renders are skipped whenever the inputs to a component, hook, or JSX expression have not changed by Object.is. It's not a runtime: by the time the browser loads the bundle, the memoization decisions are already baked in. The compiler is the same project the React team talked about for years under the name "React Forget". It reached a stable 1.0 in the React 19.1 line, and Next.js 16 was the first Next major to flip it from experimental to a first-class config flag.
What it actually emits looks a lot like the cache primitive React already uses internally. For a component like function Row({ user }), the compiler hoists each subexpression (the props object passed to a child, the className built from a template string, the inline callback handed to onClick) into a per-call cache keyed by the values it depends on. If user.name didn't change since the last render, the cache returns the previous JSX node, and React's reconciler short-circuits the subtree. In a profile, you see the same effect you used to get from wrapping every child in React.memo, but without the prop-equality boilerplate.
So, the compiler is conservative by design. If it can't statically prove that a function is safe to memoize (say, you mutate a prop, read a ref during render, or use a hook outside the top level), it leaves the file untouched and emits a bailout. The official React Compiler reference publishes the full list of bailout reasons; the ESLint plugin surfaces them in your editor as you type.
How do I enable React Compiler in Next.js 16?
Three steps: install the Babel plugin, install the ESLint plugin, and flip one flag in next.config.ts. The Babel plugin is what does the rewriting; the ESLint plugin tells you which files it'll silently skip; the Next config flag wires the plugin into Turbopack's Babel pass.
Run pnpm next dev. The first build is slower because the compiler has to walk every Client Component once; subsequent incremental builds add roughly 50–150 ms on Turbopack. I haven't seen it go above 200 ms on a 1,200-file monorepo. If you want a smoother rollout, set compilationMode: 'annotation' and add "use memo" as the first directive in the components you trust. That lets you migrate route by route instead of all at once.
Do I still need useMemo and useCallback with React Compiler?
Almost never inside components. Honestly, after enabling the compiler, the only two reasons I keep useMemo in a file are: (1) the value is shared with code outside React, for example, a memoized fetch key that I want to be identity-stable across renders for a non-React cache like SWR's mutate; or (2) the computation is genuinely expensive (parsing a 200 KB AST, building a large lookup map) and I want to be explicit about caching it across renders even if the inputs change rarely. For ordinary "I'm passing this object to a child and I don't want the child to re-render" cases, the compiler does it for you.
The before/after I show on workshop calls is a table with row selection. Before, every <Row> was wrapped in React.memo, every callback in the parent was wrapped in useCallback, every props object was assembled in a useMemo. With the compiler on, that code collapses to:
No React.memo. No useCallback. The inline arrow inside rows.map is hoisted by the compiler into a per-row cache keyed by onSelect and row.id, so clicking row 5 doesn't re-render rows 1–4. I profiled this on a 500-row table and the commit duration drops from ~28 ms to ~6 ms, well past the threshold where you can feel the input lag.
Does React Compiler work with React Server Components?
It runs only on Client Components, and that's by design. Server Components never re-render in the browser. They render once on the server and serialize to the RSC payload, so there's nothing to memoize. The compiler walks the dependency graph starting from each module that contains a 'use client' directive and rewrites only the modules reachable from there. Pure server files, including any Server Action source, are untouched, which means you can enable the compiler on a Next.js 16 app without thinking about your server boundary.
The interesting edge case is shared utilities. A ./lib/format.ts imported by both a server page and a client form will be compiled when imported from the client side and left alone when imported from the server side (Next.js 16's bundler produces two graphs). You don't have to mark anything; just keep using 'use client' the way you already do. If you're still wrapping your head around the boundary, our guide to streaming and Suspense in Next.js covers what actually crosses the network as RSC payload versus what runs as Client Component JavaScript.
What to delete from your codebase after enabling it
This is the fun part. Run pnpm next build with the compiler on once to confirm everything still works, then start a "memoization removal" branch. Here's the order I delete things in, fastest to safest:
React.memo wrappers around tiny components. If the component doesn't call into any non-React cache and its only purpose was prop-equality short-circuiting, remove the wrapper. The compiler memoizes the JSX at the call site instead, which is strictly better.
useCallback on event handlers. Search for useCallback( and delete every one that wraps an inline arrow with no captured async work. The compiler hoists these automatically.
useMemo for derived objects and arrays. Anything like useMemo(() => ({ id, label }), [id, label]) or useMemo(() => rows.filter(...), [rows]) is now redundant.
Custom equality functions passed to React.memo. If you wrote a deep-equality helper because shallow equality was failing on a nested object, that helper is dead. The compiler tracks the actual values used inside the component, not the top-level prop reference.
What to keep: useMemo for genuinely expensive pure computations (regex parsing, large array transforms with O(n²) characteristics), and useRef for anything that needs to persist across renders without triggering one. Refs are orthogonal to the compiler.
On a 240-file dashboard I migrated last month, the removal pass deleted 612 calls to useMemo/useCallback/React.memo and removed 1.8 KB of gzipped JS. The compiled output added 6.2 KB gzipped, so net we paid about 4.4 KB on the wire and gained back maintenance bandwidth. Every "do I need to memoize this?" code-review comment vanished overnight.
Measuring the wins in React DevTools
Open React DevTools, switch to the Profiler tab, and turn on "Record why each component rendered." Capture a baseline trace before flipping the compiler flag, then a second trace after. The two metrics I look at are commit duration for the interaction (clicking a row, typing in a filter input) and the count of "Did not render" entries, which are components React decided to skip. Compiler-on traces show a lot more "Did not render" rows because the cache hit lets React reuse the existing fiber.
// A useful instrumented wrapper for benchmarking specific routes.
// Drop it around the route you want to measure.
'use client';
import { Profiler, type ProfilerOnRenderCallback } from 'react';
const onRender: ProfilerOnRenderCallback = (
id, phase, actualDuration, baseDuration
) => {
// baseDuration is what it WOULD have cost without memoization.
// actualDuration is what it actually cost.
console.log({ id, phase, actualDuration, baseDuration });
};
export function MeasuredRoute({ children }: { children: React.ReactNode }) {
return <Profiler id="dashboard" onRender={onRender}>{children}</Profiler>;
}
The ratio you want to watch is actualDuration / baseDuration. With the compiler on, a typical interactive screen drops that ratio from ~0.9 (most renders happen) to ~0.2–0.3 (most renders short-circuit). For Web Vitals, the visible win is INP (Interaction to Next Paint), because the compiler trims the work done synchronously inside the click handler. I've seen p75 INP go from 280 ms to 110 ms on a heavy filter UI without any other change.
Opting out: "use no memo" and bailouts
Sometimes you have a component that breaks the Rules of React on purpose. For example, a wrapper around a third-party canvas library that needs to mutate props or read state during render. Mark the file with the "use no memo" directive at the top and the compiler leaves it alone:
'use client';
'use no memo';
import { LegacyChart } from 'some-charting-library';
export function ChartHost({ config }: { config: ChartConfig }) {
// This component mutates config during render; the compiler would
// bail out, but the directive makes the intent explicit.
return <LegacyChart config={config} />;
}
The ESLint plugin won't warn on files with "use no memo". It treats the directive as your acknowledgement that this code is opted out. In review I treat every new "use no memo" as a TODO: it usually means a Rules-of-React violation that someone should fix later, not a permanent escape hatch.
You can also scope opt-outs to a single component by passing a sources filter in next.config.ts. I've used this to keep our test fixtures uncompiled while compiling the main app, which is useful when you have a contract test that intentionally re-renders a lot to assert render counts.
Common pitfalls and how I debug them
The three failure modes I see most often.
1. "It's not memoizing my component"
Run pnpm lint. The ESLint plugin will print a "React Compiler skipped this component" warning with the rule that was violated, usually "ref accessed during render" or "prop mutated." Fix the rule, the compiler picks the component up automatically on the next build. If the warning says "early return" or "conditional hook," that's a hard violation: the compiler will refuse until the structure is fixed.
2. Stale closures inside event handlers
This is the inverse of the old useCallback bug. The compiler tracks every value referenced inside an inline function and includes them in the cache key, so the handler always sees the latest values. If you used to rely on a stale closure (rare, but it happens with debounced search), the behavior will change. Move the debounce into useEffect or use a useRef for the stale value.
3. Bundle-size regression on a specific route
If next build --analyze shows one route ballooning, look at the compiled output. Most likely you have a component that returns a huge inline JSX tree (a hand-written settings form with 80 fields) and the compiler is generating a large per-field cache. Split the form into subcomponents. The compiler memoizes at component boundaries, so smaller components mean smaller caches. Our notes on Turbopack and Next.js 16 build performance cover how to read the bundle analyzer output if you haven't done that before.
For deeper diagnostics, the React Compiler source on GitHub ships a CLI called healthcheck that walks your codebase and prints a coverage report: what percentage of your components compile cleanly, ranked worst-first. I run it once a quarter to keep an eye on Rules-of-React drift.
Frequently Asked Questions
Is React Compiler stable in 2026?
Yes. React Compiler 1.0 shipped alongside React 19.1 and is the same engine Meta has been running in production at scale since 2023. Next.js 16 moved its config from experimental to a documented option, and the major UI libraries (Radix, Headless UI, Tailwind UI) have all confirmed compatibility.
Does React Compiler replace React.memo entirely?
For almost every case, yes. The only reason to keep React.memo is if you're exporting a component from a library that will be consumed by apps that haven't enabled the compiler. Inside your own app with the compiler on, React.memo is redundant.
Can I enable React Compiler on only part of my Next.js app?
Yes. Use compilationMode: 'annotation' in next.config.ts and add 'use memo' as the top directive in the components or hooks you want compiled. Everything else is left untouched. This is how I roll it out on apps with a long tail of legacy components.
Does the compiler work with Pages Router or only App Router?
Both. The compiler operates on Client Component source, which exists in both routers. The only Next-specific wiring is the config flag; the Babel plugin itself is router-agnostic.
Will React Compiler slow down my dev server?
Cold start adds 1–3 seconds on a typical app because the compiler has to traverse every Client Component once. Hot updates add roughly 50–150 ms per file on Turbopack, imperceptible in practice. Production builds add 5–10% to the build time, which is a one-time cost per deploy.
Enable Next.js Draft Mode in the App Router with draftMode(), signed __prerender_bypass cookies, and secure preview URLs for Sanity, Contentful, and DatoCMS.