Next.js & React

React Compiler in Next.js 16.4: Babel vs Rust, Benchmarked

I compiled a Next.js 16.4 app with no compiler, the Babel React Compiler and the new Rust React Compiler, then counted re-renders in Chrome: 20,000 row renders dropped to 0, build cost, bundle size, lint rules and the memo() gap.

React Compiler in Next.js cover: next.config.ts with reactCompiler true and turbopackRustReactCompiler, plus row re-renders falling from 20,000 to 0
On this page
  1. TL;DR: should I turn on the React Compiler in Next.js?
  2. What is the React Compiler?
  3. How do I enable the React Compiler in Next.js 16?
  4. Can I enable it only for some components?
  5. What is the Rust React Compiler in Next.js?
  6. How I tested it
  7. Does the React Compiler reduce re-renders?
  8. Why do list rows still re-render with the compiler?
  9. What does the compiled code look like?
  10. Does the React Compiler slow down next build?
  11. Which code does the React Compiler skip?
  12. How do I find code the compiler can't optimize?
  13. Step by step: adopt the React Compiler in an existing Next.js app
  14. Should I remove useMemo and useCallback?
  15. Common React Compiler mistakes in Next.js
  16. Key takeaways

React Compiler is a build-time tool from the React team that automatically memoizes your components and hooks, so you no longer write most useMemo, useCallback and memo calls by hand. In Next.js it is one config flag, reactCompiler: true, stable since Next.js 16.0. Next.js 16.4 adds an experimental native Rust version.

I built a Next.js 16.4.2 test app and compiled it three ways: compiler off, the Babel compiler, and the Rust compiler. Then I counted re-renders in a real browser. With the compiler on, 20 clicks on a counter caused 0 list-row re-renders instead of 20,000, and scripting time fell from about 60 ms to about 7 ms. The Babel version more than doubled compile time. The Rust version cost about a quarter of a second and produced byte-identical output.

TL;DR: should I turn on the React Compiler in Next.js?#

Yes, for most App Router projects. Turn it on with reactCompiler: true, run the compiler lint rules first, and try experimental.turbopackRustReactCompiler if you build with Turbopack. In my test the compiler removed almost all wasted re-renders for free. It does not replace memo on list items whose parent re-renders, and it skips code that breaks the Rules of React.

Compiler offBabel compilerRust compiler (experimental)
ConfigdefaultreactCompiler: true+ experimental.turbopackRustReactCompiler: true
Extra installnonebabel-plugin-react-compilernone
Compile step (median, my app)2.25 s5.65 s2.5 s
Full next build (median)7.7 s11.1 s8.2 s
Client JS, gzip178,305 B181,295 B181,295 B (identical to Babel)
Row re-renders, 20 counter clicks20,00000
Works with webpackyesyesno (Turbopack only)

What is the React Compiler?#

The React Compiler is a build-time tool that rewrites your components so they cache values, callbacks and JSX between renders, and recompute them only when their inputs change. The React Compiler v1.0 announcement calls it "a build-time tool that optimizes your React app through automatic memoization." Version 1.0 shipped on October 7, 2025.

It works with React 17 and later, and on React Native. The introduction docs say that in most cases its memoization "will be as precise, or moreso, than what you may have written." Meta reports that in the Meta Quest Store, "initial loads and cross-page navigations improve by up to 12%, while certain interactions are more than 2.5× faster."

You keep writing plain components. The compiler does the bookkeeping at build time.

How do I enable the React Compiler in Next.js 16?#

Install the Babel plugin and set the top-level reactCompiler option in next.config.ts. It moved out of experimental in Next.js 16.0. Next.js does not run Babel on every file: per the reactCompiler docs, "Next.js uses a custom SWC optimization that only applies the React Compiler to relevant files—like those with JSX or React Hooks."

bash
npm install -D babel-plugin-react-compiler@1.0.0
ts
// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  reactCompiler: true,
};

export default nextConfig;

That is the whole setup. New projects can also pick the compiler in the create-next-app prompts.

Can I enable it only for some components?#

Yes. Set compilationMode: "annotation" and add the "use memo" directive to the components or hooks you want compiled. This is a good way to start on a large codebase. Add "use no memo" to a component to opt it out in the default mode while you fix it.

ts
const nextConfig: NextConfig = {
  reactCompiler: { compilationMode: "annotation" },
};
tsx
export default function Dashboard() {
  "use memo";
  // ...
}

What is the Rust React Compiler in Next.js?#

It is a native Rust port of the React Compiler that runs inside Turbopack instead of through Babel in Node.js. It was merged into the React repo in June 2026 (PR #36173) and added to Next.js 16.3 as an experimental option. The Next.js 16.4 release added a fast check that skips files that don't need compiling.

Next.js 16.4 says the Turbopack team's memory changes "reduced the React Compiler's memory usage by 30% and compile time by 15%." To turn it on:

ts
// next.config.ts
const nextConfig: NextConfig = {
  reactCompiler: true,
  experimental: {
    turbopackRustReactCompiler: true,
  },
};

From the turbopackRustReactCompiler docs:

  • It "requires reactCompiler to be enabled". It picks the implementation but doesn't switch the compiler on.
  • It works only with Turbopack. "Using it with webpack will throw an error."
  • You do not need babel-plugin-react-compiler installed.

How I tested it#

I wanted numbers from a real Next.js app, not a toy counter, so I measured both build cost and runtime behaviour.

  1. App: Next.js 16.4.2, React 19.3.1, Turbopack, Node 24, on an 8-vCPU Linux box. The client dashboard has 1,000 product rows, a filter input, a counter, a stats panel and one deliberately "bad" component. Nothing uses useMemo or useCallback.
  2. Build load: 300 more generated client components on a /gen route, so compile time is measurable.
  3. Three builds: one next.config.ts read an RC_MODE variable (off, babel, rust). I deleted .next before every build, so the Turbopack disk cache never helped, and ran 5 rounds. The tables use the median of rounds 2-5. Round 1 was a cold start: 3.3 s, 12.6 s and 2.9 s of compile time.
  4. Re-renders: each component calls a tiny useRenderCount(name) hook that increments a counter in an effect. Puppeteer drove headless Chrome against next start, and Chrome's ScriptDuration metric measured JavaScript time. I ran each scenario 3 times, and the render counts were identical every run.
ts
// lib/useRenderCount.ts
"use client";
import { useEffect } from "react";

export function useRenderCount(name: string) {
  useEffect(() => {
    const r = (window.__renders ??= {});
    r[name] = (r[name] ?? 0) + 1;
  });
}

The counter lives in an effect on purpose. Writing to a global during render breaks the Rules of React, and the compiler would skip that component.

Does the React Compiler reduce re-renders?#

Yes, dramatically for state that doesn't touch a component's props. Clicking the counter 20 times re-rendered every component on the page with the compiler off: 20,000 row renders. With either compiler, only the two components that actually use the count re-rendered. Rows, stats, the list and even the uncompilable component were all skipped.

React Compiler re-render test in Next.js 16.4.2: 20 counter clicks caused 20,000 row renders with the compiler off and 0 with it on

Scenario (renders counted)OffCompilerCompiler + memo(Row)
20 counter clicks: Row renders20,00000
20 counter clicks: JS time55-62 ms6-7 ms6-7 ms
Type "99" in filter: Row renders2902900
Select 10 rows: Row renders10,00010,00019
Select 10 rows: JS time26-31 ms14 ms8 ms

The Babel and Rust builds produced exactly the same render counts, which you would expect from identical output.

The click case is the common one in real apps. Some unrelated state changes high in the tree, and with the compiler off everything below it re-renders.

Why do list rows still re-render with the compiler?#

Because the compiler memoizes inside components. It does not turn every component into React.memo. When ProductList must re-render, for example because selectedId changed, its .map() creates new <Row> elements and each row runs. The compiled Row is cheaper to run, since its internals are cached, so JS time still halved from about 28 ms to 14 ms. But all 1,000 rows rendered on each click.

Wrapping the row in memo() fixed it. Selecting 10 rows then rendered 19 rows (1 + 9×2: the newly selected row and the previously selected one).

tsx
import { memo } from "react";

function Row({ item, selected, onSelect }: RowProps) {
  return (
    <li style={{ fontWeight: selected ? 700 : 400 }} onClick={() => onSelect(item.id)}>
      {item.name}
    </li>
  );
}

const MemoRow = memo(Row); // still worth it for big lists

Without the compiler, the same memo(Row) did nothing: still 10,000 renders. The parent passes onSelect={(id) => setSelectedId(id)} as a new function every render, which breaks memo's prop comparison. The compiler caches that callback automatically. That pairing is what makes memo reliable: compiler for stable props, memo for big lists.

What does the compiled code look like?#

The compiler adds a small cache array per component (_c(n) from react/compiler-runtime) and wraps each computation in an "inputs changed?" check. This is what the Babel plugin produced for my Stats component, which totals prices on every render in the source:

js
function Stats(t0) {
  const $ = _c(10);
  const { items } = t0;
  let onSale, t1, t2, t3;
  if ($[0] !== items) {
    const total = items.reduce(_temp, 0);
    onSale = items.filter(_temp2).length;
    // ...store results in $[1..4]
  } else {
    onSale = $[1]; // reuse cached values
    // ...
  }
  // the returned <p> is cached the same way
}

The expensive reduce and filter run only when items changes. This is the same thing you would do by hand with useMemo, applied to every value in every component.

Does the React Compiler slow down next build?#

The Babel version does, the Rust version barely does. In my 300-component app the compile step went from 2.25 s with the compiler off to 5.65 s with Babel, about 2.5 times longer. The Rust compiler took 2.5 s, only 0.25 s more than no compiler at all. Full next build wall time was 7.7 s, 11.1 s and 8.2 s.

Babel vs Rust React Compiler build cost: compile 2.25 s off, 5.65 s Babel, 2.5 s Rust; full next build 7.7 s, 11.1 s and 8.2 s

Both compilers produced byte-identical client JavaScript: the same chunk hashes and the same sizes. Compiled code is larger, because every component now carries cache checks. Raw client JS grew by 213,496 bytes (+28.9%), but after gzip the difference was only 2,990 bytes (+1.7%). The cache checks are repetitive and compress well.

These are numbers from one small app on one machine. Your ratio will differ with codebase size, but the direction should hold: Babel adds noticeable compile time and Rust mostly removes it.

Which code does the React Compiler skip?#

Components that break the Rules of React. The compiler checks your code, and if it can't prove a component is safe to memoize it leaves that component uncompiled instead of guessing. The build does not fail. My Bailout component reads and writes ref.current during render, and the Babel plugin's logger reported CompileError: Cannot access refs during render.

tsx
export function Bailout({ label }: { label: string }) {
  const renders = useRef(0);
  renders.current += 1; // breaks the rules: ref mutation during render
  return <aside>{label} (render #{renders.current})</aside>;
}

Notice from the render table that Bailout was still skipped on counter clicks. The compiled parent cached the <Bailout label="legacy widget" /> element, so React never re-rendered it. Code the compiler skips simply misses its own internal optimizations. It doesn't break.

How do I find code the compiler can't optimize?#

Use the latest eslint-plugin-react-hooks (7.x today). Since React Compiler 1.0, the compiler-powered lint rules ship in the plugin's recommended preset: refs, purity, immutability, set-state-in-effect, set-state-in-render, globals, static-components, incompatible-library and more. You don't need the separate eslint-plugin-react-compiler package any more.

js
// eslint.config.mjs
import reactHooks from "eslint-plugin-react-hooks";
import tseslint from "typescript-eslint";

export default [
  ...tseslint.configs.recommended,
  reactHooks.configs.flat.recommended,
];

On my test app, eslint-plugin-react-hooks 7.1.1 flagged the Bailout ref access three times. It also caught a real bug pattern in my generator: 300 components called setState inside useEffect. The rule message says this "can trigger cascading renders". I fixed the generator before benchmarking.

Run the linter before turning on the compiler, fix what it reports, and treat the warnings as a list of components that won't be optimized.

Step by step: adopt the React Compiler in an existing Next.js app#

  1. Upgrade. Use Next.js 16.x and React 19.x. The compiler also supports React 17 and 18 through react-compiler-runtime.
  2. Lint first. Add reactHooks.configs.flat.recommended, run eslint . and fix or note the errors.
  3. Install and enable. Run npm i -D babel-plugin-react-compiler, then set reactCompiler: true. On a large app, start with compilationMode: "annotation" and add "use memo" to hot paths.
  4. Try Rust on Turbopack. Add experimental.turbopackRustReactCompiler: true and compare next build times.
  5. Test behaviour. Run your end-to-end tests. Watch effects that depend on memoized values.
  6. Keep memo on big lists. The compiler stabilizes props. memo still decides whether a list item renders.
  7. Remove manual memoization slowly. The React team advises keeping existing useMemo and useCallback or testing carefully before removing them, because removal can change compilation output.

Should I remove useMemo and useCallback?#

Not in a hurry. For new code, write plain components and let the compiler memoize. In existing code, leave useMemo and useCallback alone until you have tests. The React docs say the hooks "can continue to be used with React Compiler as an escape hatch", for example to keep an effect dependency stable. Removing them can change what the compiler outputs.

Common React Compiler mistakes in Next.js#

  • Expecting it to fix big lists alone. It doesn't add React.memo to children. Keep memo() on heavy list items.
  • Mutating during render. Ref writes, global writes or prop mutation make the compiler skip the component, with no build error.
  • Using the Rust flag with webpack. turbopackRustReactCompiler throws with webpack. Remove it or switch to Turbopack.
  • Enabling it without the lint rules. You won't know which components were skipped.
  • Benchmarking with a warm cache. Turbopack's build disk cache hides compile cost. Delete .next between comparison builds.
  • Expecting Server Component wins. The compiler targets components that re-render on the client. Server Components render once per request.

If you are on Next.js 16.4, pair this with the other new client-side features in my guides to React View Transitions in Next.js and the use cache directive.

Key takeaways#

  • React Compiler 1.0 automatically memoizes components and hooks at build time; in Next.js 16 enable it with reactCompiler: true.
  • In my Next.js 16.4.2 test, 20 counter clicks caused 0 row re-renders with the compiler versus 20,000 without, and JS time fell from about 60 ms to 7 ms.
  • The compiler does not make children React.memo; list rows still re-rendered until I wrapped them in memo(), which then cut 10,000 renders to 19.
  • The Babel compiler made the compile step 2.5x longer (2.25 s to 5.65 s); the experimental Rust compiler took 2.5 s.
  • Babel and Rust compilers produced byte-identical client JavaScript; gzipped size grew only 1.7%.
  • experimental.turbopackRustReactCompiler needs reactCompiler: true, works only with Turbopack, and needs no Babel plugin.
  • Run eslint-plugin-react-hooks recommended rules first; components that break the Rules of React are silently skipped.

Questions