DEV Community

Cover image for React Compiler in Next.js 16.4: Babel vs Rust, Benchmarked
WaseemAnjum
WaseemAnjum

Posted on Originally published at fabwaseem.com

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 experimental Rust one, then counted re-renders in Chrome. The original, with any updates, lives on fabwaseem.com.

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 off Babel compiler Rust compiler (experimental)
Config default reactCompiler: true + experimental.turbopackRustReactCompiler: true
Extra install none babel-plugin-react-compiler none
Compile step (median, my app) 2.25 s 5.65 s 2.5 s
Full next build (median) 7.7 s 11.1 s 8.2 s
Client JS, gzip 178,305 B 181,295 B 181,295 B (identical to Babel)
Row re-renders, 20 counter clicks 20,000 0 0
Works with webpack yes yes no (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."

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

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

export default nextConfig;
Enter fullscreen mode Exit fullscreen mode

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.

const nextConfig: NextConfig = {
  reactCompiler: { compilationMode: "annotation" },
};
Enter fullscreen mode Exit fullscreen mode
export default function Dashboard() {
  "use memo";
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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:

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

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.
// 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;
  });
}
Enter fullscreen mode Exit fullscreen mode

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) Off Compiler Compiler + memo(Row)
20 counter clicks: Row renders 20,000 0 0
20 counter clicks: JS time 55-62 ms 6-7 ms 6-7 ms
Type "99" in filter: Row renders 290 290 0
Select 10 rows: Row renders 10,000 10,000 19
Select 10 rows: JS time 26-31 ms 14 ms 8 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).

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
Enter fullscreen mode Exit fullscreen mode

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:

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
}
Enter fullscreen mode Exit fullscreen mode

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.

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>;
}
Enter fullscreen mode Exit fullscreen mode

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.

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

export default [
  ...tseslint.configs.recommended,
  reactHooks.configs.flat.recommended,
];
Enter fullscreen mode Exit fullscreen mode

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.

Related: the React Compiler lint rules from eslint-plugin-react-hooks 7 don't all survive a linter switch. Oxlint ported 12 of 14, Biome none; see Oxlint vs Biome vs ESLint.

Related: wondering whether to leave Next.js entirely? In TanStack Start vs Next.js I built the same app in both: TanStack Start shipped 111 KB of gzipped JS to Next.js 16.4's 139 KB and built in 1.26 s vs 4.86 s.

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.

FAQ

What is the React Compiler?

The React Compiler is a build-time tool from the React team that automatically memoizes components and hooks. It rewrites your code to cache values, callbacks and JSX between renders and recompute them only when inputs change, replacing most manual useMemo, useCallback and memo calls. Version 1.0 shipped on October 7, 2025, and it supports React 17 and later.

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

Install babel-plugin-react-compiler as a dev dependency and set reactCompiler: true at the top level of next.config.ts. The option became stable in Next.js 16.0, moving out of experimental. Next.js applies the compiler only to files with JSX or hooks, using an SWC pre-check, to keep build cost down. Use compilationMode: 'annotation' to opt in per component.

What is turbopackRustReactCompiler?

It is an experimental Next.js option, added in 16.3, that runs the native Rust port of the React Compiler inside Turbopack instead of the Babel plugin. It requires reactCompiler: true, works only with Turbopack and throws with webpack, and needs no babel-plugin-react-compiler install. In my test it compiled in 2.5 seconds versus 5.65 seconds for Babel.

Does the React Compiler make next build slower?

With the Babel plugin, noticeably. In my 300-component Next.js 16.4.2 app the compile step went from 2.25 seconds to 5.65 seconds, and full next build from 7.7 to 11.1 seconds. The experimental Rust compiler took 2.5 seconds to compile and 8.2 seconds in total, so most of the cost disappeared.

Does the React Compiler replace React.memo?

Not for list items. The compiler memoizes inside each component and caches child elements, but when a parent re-renders and maps over data, every child still runs. In my test, selecting a row re-rendered all 1,000 rows with the compiler alone. Wrapping the row in memo() cut 10,000 renders to 19, because the compiler kept its props stable.

Should I remove useMemo and useCallback when using the React Compiler?

Not right away. Write new code without them, but keep existing calls until you have tests, because the React team notes that removing them can change compilation output. The hooks still work as an escape hatch, for example to keep an effect dependency stable so the effect doesn't fire repeatedly when its inputs haven't meaningfully changed.

What happens if my code breaks the Rules of React?

The compiler skips that component and leaves it unoptimized; the build does not fail. In my test, a component that mutated ref.current during render produced a 'Cannot access refs during render' compile error in the logger and was left as written. The eslint-plugin-react-hooks recommended rules report the same problems in your editor.

Does the React Compiler increase bundle size?

Slightly. Compiled components carry cache checks, so in my test raw client JavaScript grew 28.9 percent, by 213,496 bytes, but gzipped size grew only 1.7 percent, by 2,990 bytes, because the generated code compresses well. The Babel and Rust compilers produced byte-identical bundles in Next.js 16.4.2.


Originally published on fabwaseem.com.

Top comments (0)