← Back to blog

React Compiler: Automatic Memoization

March 29, 2026·9 min read
ReactNext.jsTypeScriptWeb Performance

If you have spent any meaningful time optimizing React applications, you know the drill: wrap expensive computations in useMemo, stabilize callback references with useCallback, and sprinkle React.memo on components that re-render too often. It works, but it is tedious, error prone, and clutters your codebase with optimization logic that has nothing to do with your actual features.

React Compiler changes this entirely. Released as a stable v1.0 in October 2025, it is a build time tool that analyzes your components and automatically inserts memoization where it matters. No more manual optimization hooks. No more guessing which dependency arrays are correct. In this post, I will walk through what React Compiler actually does, how to set it up in a Next.js 16 project, and the real performance wins you can expect.

What React Compiler Actually Does

Let me be precise here, because there is a lot of confusion online. React Compiler is not a bundler. It does not do tree-shaking, code splitting, or dead code elimination. Those are the responsibilities of your bundler (Webpack, Turbopack, Rollup, Vite).

React Compiler is a Babel plugin (babel-plugin-react-compiler) that runs at build time and automatically applies memoization to your components. Specifically, it:

  1. Analyzes your component's render logic to understand which values and expressions depend on which props and state
  2. Automatically memoizes values that would otherwise be recomputed on every render (the equivalent of useMemo)
  3. Automatically memoizes callbacks to maintain referential stability (the equivalent of useCallback)
  4. Automatically memoizes JSX output so child components skip re-rendering when their props have not changed (the equivalent of React.memo)

The compiler understands the Rules of React, specifically that components should be pure functions of their props and state, and uses this knowledge to determine what can be safely cached.

Before and After: A Real Example

Consider a component that filters and sorts a large list:

"use client";
 
import { useState } from "react";
 
type Product = {
  id: string;
  name: string;
  price: number;
  category: string;
};
 
type ProductListProps = {
  products: Product[];
};
 
export function ProductList({ products }: ProductListProps) {
  const [search, setSearch] = useState("");
  const [sortBy, setSortBy] = useState<"name" | "price">("name");
 
  // Without React Compiler, this runs on EVERY render,
  // even when only `search` changed and `sortBy` stayed the same.
  const filtered = products.filter((p) =>
    p.name.toLowerCase().includes(search.toLowerCase())
  );
 
  const sorted = [...filtered].sort((a, b) => {
    if (sortBy === "price") return a.price - b.price;
    return a.name.localeCompare(b.name);
  });
 
  return (
    <div>
      <input
        value={search}
        onChange={(e) => setSearch(e.target.value)}
        placeholder="Search products..."
        className="border p-2 rounded mb-4 w-full"
      />
      <div className="flex gap-2 mb-4">
        <button onClick={() => setSortBy("name")}>Sort by Name</button>
        <button onClick={() => setSortBy("price")}>Sort by Price</button>
      </div>
      <ul>
        {sorted.map((product) => (
          <ProductCard key={product.id} product={product} />
        ))}
      </ul>
    </div>
  );
}
 
function ProductCard({ product }: { product: Product }) {
  return (
    <li className="p-4 border rounded mb-2">
      <h3 className="font-bold">{product.name}</h3>
      <p className="text-gray-600">${product.price.toFixed(2)}</p>
    </li>
  );
}

Without React Compiler, you would need to manually optimize this:

const filtered = useMemo(
  () => products.filter((p) =>
    p.name.toLowerCase().includes(search.toLowerCase())
  ),
  [products, search]
);
 
const sorted = useMemo(
  () => [...filtered].sort((a, b) => {
    if (sortBy === "price") return a.price - b.price;
    return a.name.localeCompare(b.name);
  }),
  [filtered, sortBy]
);

With React Compiler, you write the plain version (the first example) and the compiler inserts the equivalent memoization automatically during the build. The output code is not identical to hand-written useMemo calls; it uses an internal caching mechanism that is more granular and often more efficient.

Setting Up React Compiler in Next.js 16

Next.js 16 has first-class support for React Compiler. Here is how to enable it.

Step 1: Install the Compiler Plugin

npm install -D babel-plugin-react-compiler

This is the only additional dependency you need. The compiler itself is a standalone tool maintained by the React team.

Step 2: Enable It in next.config.js

// next.config.js
module.exports = {
  reactCompiler: true,
};

In Next.js 16, this is a stable, top-level configuration option. If you are coming from Next.js 15, note that it was previously under experimental.reactCompiler; that is no longer needed.

Step 3: Verify It Is Working

Run your dev server and check the terminal output:

npm run dev

Turbopack (the default bundler in Next.js 16) will show compiler output in the build logs. You can also inspect the compiled output by looking at the transformed code in your browser's DevTools sources panel. Memoized values will appear as cached variables with names like $[0], $[1], etc.

What React Compiler Expects from Your Code

React Compiler is not magic. It relies on your components following the Rules of React. If your code violates these rules, the compiler will either skip optimization for that component or potentially produce incorrect behavior.

Rule 1: Components Must Be Pure

Components should return the same JSX given the same props and state. Side effects belong in useEffect, event handlers, or Server Components.

"use client";
 
// GOOD: Pure component, compiler can optimize this
function Greeting({ name }: { name: string }) {
  return <h1>Hello, {name}</h1>;
}
 
// BAD: Mutates external state during render
let renderCount = 0;
function BadCounter({ count }: { count: number }) {
  renderCount++; // Side effect during render!
  return <p>Count: {count}</p>;
}

Rule 2: Do Not Mutate Props or State Directly

The compiler assumes values are immutable between renders. Mutating objects or arrays in place breaks this assumption.

"use client";
 
import { useState } from "react";
 
// GOOD: Creates a new array
function TodoList() {
  const [todos, setTodos] = useState<string[]>([]);
 
  const addTodo = (text: string) => {
    setTodos((prev) => [...prev, text]);
  };
 
  return <div>{todos.map((t, i) => <p key={i}>{t}</p>)}</div>;
}
 
// BAD: Mutates the existing array
function BadTodoList() {
  const [todos, setTodos] = useState<string[]>([]);
 
  const addTodo = (text: string) => {
    todos.push(text); // Mutation!
    setTodos(todos);   // Same reference, compiler cache will be stale
  };
 
  return <div>{todos.map((t, i) => <p key={i}>{t}</p>)}</div>;
}

Rule 3: Hooks Must Be Called Unconditionally

This is a standard React rule, but it matters even more with the compiler because it tracks dependencies at the hook level.

"use client";
 
import { useState, useEffect } from "react";
 
// BAD: Conditional hook call
function UserProfile({ userId }: { userId: string | null }) {
  if (!userId) return <p>No user</p>;
 
  // This hook call is conditional, which violates the Rules of Hooks
  const [user, setUser] = useState(null);
 
  return <p>{user}</p>;
}

Using the ESLint Plugin

The React team provides an ESLint plugin that catches violations of these rules before the compiler even runs:

npm install -D eslint-plugin-react-compiler
// eslint.config.js
import reactCompiler from "eslint-plugin-react-compiler";
 
export default [
  {
    plugins: {
      "react-compiler": reactCompiler,
    },
    rules: {
      "react-compiler/react-compiler": "error",
    },
  },
];

This catches issues during development so you do not discover them in production.

Combining React Compiler with Server Components

One of the most effective performance strategies in Next.js 16 is combining React Compiler with Server Components. The key insight is that React Compiler optimizes client-side re-rendering, while Server Components eliminate client-side rendering entirely for data-fetching components.

Here is a pattern I use frequently in my projects:

// app/dashboard/page.tsx (Server Component, the default)
async function DashboardPage() {
  const stats = await fetch("https://api.example.com/stats", {
    next: { revalidate: 60 },
  });
  const data = await stats.json();
 
  return (
    <div className="p-8">
      <h1 className="text-2xl font-bold mb-6">Dashboard</h1>
      <StatsGrid stats={data.summary} />
      <InteractiveChart dataPoints={data.timeSeries} />
    </div>
  );
}
 
export default DashboardPage;
// app/dashboard/stats-grid.tsx (also a Server Component)
type StatsGridProps = {
  stats: { label: string; value: number }[];
};
 
function StatsGrid({ stats }: StatsGridProps) {
  return (
    <div className="grid grid-cols-3 gap-4 mb-8">
      {stats.map((stat) => (
        <div key={stat.label} className="p-4 bg-white rounded shadow">
          <p className="text-sm text-gray-500">{stat.label}</p>
          <p className="text-2xl font-bold">{stat.value.toLocaleString()}</p>
        </div>
      ))}
    </div>
  );
}
 
export default StatsGrid;
"use client";
 
// app/dashboard/interactive-chart.tsx (Client Component, React Compiler optimizes this)
import { useState } from "react";
 
type DataPoint = { date: string; value: number };
 
type InteractiveChartProps = {
  dataPoints: DataPoint[];
};
 
export function InteractiveChart({ dataPoints }: InteractiveChartProps) {
  const [range, setRange] = useState<"7d" | "30d" | "90d">("30d");
 
  // React Compiler automatically memoizes this filtering logic
  const filteredData = dataPoints.filter((point) => {
    const daysAgo = Math.floor(
      (Date.now() - new Date(point.date).getTime()) / (1000 * 60 * 60 * 24)
    );
    const maxDays = range === "7d" ? 7 : range === "30d" ? 30 : 90;
    return daysAgo <= maxDays;
  });
 
  const maxValue = Math.max(...filteredData.map((d) => d.value));
 
  return (
    <div>
      <div className="flex gap-2 mb-4">
        {(["7d", "30d", "90d"] as const).map((r) => (
          <button
            key={r}
            onClick={() => setRange(r)}
            className={`px-3 py-1 rounded ${
              range === r ? "bg-blue-500 text-white" : "bg-gray-200"
            }`}
          >
            {r}
          </button>
        ))}
      </div>
      <div className="flex items-end gap-1 h-48">
        {filteredData.map((point) => (
          <div
            key={point.date}
            className="bg-blue-400 rounded-t flex-1"
            style={{ height: `${(point.value / maxValue) * 100}%` }}
            title={`${point.date}: ${point.value}`}
          />
        ))}
      </div>
    </div>
  );
}

The architecture is clear: DashboardPage and StatsGrid are Server Components that fetch and render data with zero client-side JavaScript. InteractiveChart is a Client Component where React Compiler automatically handles memoization of the filtering and max-value calculations.

Measuring the Impact

React Compiler's benefits are most visible on components with:

  • Expensive derived state (filtering, sorting, transforming large datasets)
  • Frequent re-renders (components near the top of the tree, or those affected by rapidly changing context)
  • Deep component trees where unnecessary re-renders cascade through many children

Profiling with React DevTools

React DevTools (v5+) highlights components that React Compiler has optimized. In the Profiler tab, you will see "Compiler" badges on optimized components. This makes it straightforward to verify the compiler is working as expected.

A practical approach I follow when working on performance for my services:

  1. Profile first: Use React DevTools Profiler to identify which components re-render unnecessarily
  2. Enable the compiler: Turn on reactCompiler: true and re-profile
  3. Measure Core Web Vitals: Use Lighthouse or the Web Vitals library to measure INP (Interaction to Next Paint), which directly reflects re-render performance
  4. Address remaining issues: If a component is still slow after compilation, the problem is likely in the render logic itself (expensive DOM operations, large lists without virtualization) rather than unnecessary re-renders

What React Compiler Does Not Fix

It is important to set realistic expectations. React Compiler will not help with:

  • Network waterfalls: Use Server Components, parallel data fetching, or <Suspense> boundaries instead
  • Large bundle sizes: That is your bundler's job (Turbopack handles this in Next.js 16)
  • Slow initial page loads: Optimize with Server Components, streaming SSR, and proper code splitting via next/dynamic or React.lazy
  • Layout thrashing or expensive DOM operations: These require algorithmic improvements or virtualization (e.g., react-window or @tanstack/virtual)

Opting Out When Needed

Occasionally, you may need to tell the compiler to skip a specific component. The "use no memo" directive does this:

"use client";
 
export function CanvasRenderer({ data }: { data: number[] }) {
  "use no memo";
 
  // This component intentionally mutates a canvas ref on every render.
  // React Compiler's memoization would break this behavior.
  // ...
}

Use this sparingly. If you find yourself adding it frequently, that is a sign your code may be violating the Rules of React.

Deploying to Production

When deploying a Next.js 16 app with React Compiler to Vercel, there is no additional configuration needed. Vercel's build pipeline picks up the reactCompiler: true setting and applies the compiler during the production build.

For CI/CD pipelines (Azure DevOps, GitHub Actions), ensure your build step uses Node.js 22 LTS (or minimum 20.9) and that babel-plugin-react-compiler is in your devDependencies. The compiler runs as part of the standard next build command, so no extra build steps are required.

Conclusion

React Compiler is the most significant developer experience improvement in the React ecosystem since hooks. By automatically handling memoization at build time, it eliminates an entire category of manual optimization work while often producing more granular caching than hand-written useMemo and useCallback calls. Combined with Server Components for data-heavy rendering and Turbopack for fast builds, it forms a powerful performance stack in Next.js 16. The key takeaway: write clean, idiomatic React that follows the Rules of React, enable the compiler, and let it do the optimization work for you.

Related Posts