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

**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](https://react.dev/blog/2025/10/07/react-compiler-1) 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](https://react.dev/learn/react-compiler/introduction) 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](https://nextjs.org/docs/app/api-reference/config/next-config-js/reactCompiler), "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](https://github.com/facebook/react/pull/36173)) and added to Next.js 16.3 as an experimental option. The [Next.js 16.4 release](https://nextjs.org/blog/next-16-4) 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](https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopackRustReactCompiler):

- 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](https://res.cloudinary.com/wasicloudinary/image/upload/v1792253627/fabwaseem/blog/react-compiler-nextjs/diagram-renders.jpg)

| 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).

```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](https://res.cloudinary.com/wasicloudinary/image/upload/v1792253628/fabwaseem/blog/react-compiler-nextjs/diagram-build.jpg)

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](https://react.dev/reference/eslint-plugin-react-hooks) 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](https://www.fabwaseem.com/blog/react-view-transitions) and the [use cache directive](https://www.fabwaseem.com/blog/nextjs-use-cache).

**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](https://www.fabwaseem.com/blog/oxlint-vs-eslint).

**Related:** wondering whether to leave Next.js entirely? In [TanStack Start vs Next.js](https://www.fabwaseem.com/blog/tanstack-start-vs-nextjs) 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.
