React Compiler with Next.js: Automatic Optimization Guide

Prerequisites
Before starting this tutorial, make sure you have:
- Node.js 20+ installed
- Next.js 15+ project (App Router recommended)
- Basic understanding of React hooks and memoization (
useMemo,useCallback,React.memo) - A code editor like VS Code
What Is React Compiler?
React Compiler (formerly known as React Forget) is a build-time tool that automatically optimizes your React components by inserting memoization where needed. Instead of manually wrapping values in useMemo, callbacks in useCallback, and components in React.memo, the compiler analyzes your code and does it for you.
This means:
- No more manual memoization — the compiler handles it
- Smaller bundle sizes — unnecessary re-renders are eliminated at build time
- Better performance by default — every component is optimized without developer effort
- Cleaner code — remove boilerplate
useMemo/useCallbackwrappers
React Compiler understands the Rules of React and safely transforms your code while preserving correctness.
What You'll Build
In this tutorial, you will:
- Set up React Compiler in a Next.js 15 project
- Configure the Babel plugin and ESLint integration
- Understand what the compiler optimizes and how
- Debug compiled output to verify optimizations
- Measure real-world performance improvements
- Handle edge cases and opt-out scenarios
Step 1: Create a Next.js Project
If you already have a Next.js 15+ project, skip to Step 2. Otherwise, create a new one:
npx create-next-app@latest react-compiler-demo --typescript --tailwind --app --src-dir
cd react-compiler-demoVerify your React version is 19 or later:
npm ls reactYou should see react@19.x.x. React Compiler requires React 19+.
Step 2: Install React Compiler
Install the compiler Babel plugin and ESLint plugin:
npm install -D babel-plugin-react-compiler eslint-plugin-react-compilerThese two packages are all you need:
babel-plugin-react-compiler— the core compiler that transforms your code at build timeeslint-plugin-react-compiler— surfaces violations of the Rules of React that would prevent safe compilation
Step 3: Configure Next.js
Next.js 15 has built-in support for React Compiler. Enable it in your next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
reactCompiler: true,
},
};
export default nextConfig;That single line is enough to enable the compiler for your entire project. Next.js handles the Babel plugin integration automatically.
Advanced Configuration
For more control, you can pass options:
const nextConfig: NextConfig = {
experimental: {
reactCompiler: {
compilationMode: "annotation", // Only compile opted-in components
panicThreshold: "CRITICAL_ERRORS", // Fail build on critical issues
},
},
};The compilationMode options are:
| Mode | Description |
|---|---|
"infer" | Default. Compiles all components and hooks automatically |
"annotation" | Only compiles components/hooks with "use memo" directive |
"all" | Compiles everything, even if it violates Rules of React (use with caution) |
For most projects, the default "infer" mode is the right choice.
Step 4: Set Up ESLint Integration
Add the ESLint plugin to catch Rules of React violations early. Update your .eslintrc.json:
{
"plugins": ["react-compiler"],
"rules": {
"react-compiler/react-compiler": "error"
}
}Or if you use the flat ESLint config (eslint.config.mjs):
import reactCompiler from "eslint-plugin-react-compiler";
export default [
{
plugins: {
"react-compiler": reactCompiler,
},
rules: {
"react-compiler/react-compiler": "error",
},
},
];Now run ESLint to check your codebase:
npx next lintAny violations will appear as errors, telling you exactly what to fix before the compiler can safely optimize your code.
Step 5: Understanding What Gets Optimized
Let's look at a practical example. Consider this component before React Compiler:
// components/ProductList.tsx — before optimization
"use client";
import { useState } from "react";
interface Product {
id: number;
name: string;
price: number;
category: string;
}
function ProductCard({ product }: { product: Product }) {
console.log(`Rendering ProductCard: ${product.name}`);
return (
<div className="border rounded-lg p-4">
<h3 className="font-bold">{product.name}</h3>
<p className="text-gray-600">${product.price}</p>
</div>
);
}
function CategoryFilter({
categories,
selected,
onSelect,
}: {
categories: string[];
selected: string;
onSelect: (cat: string) => void;
}) {
console.log("Rendering CategoryFilter");
return (
<div className="flex gap-2 mb-4">
{categories.map((cat) => (
<button
key={cat}
onClick={() => onSelect(cat)}
className={selected === cat ? "bg-blue-500 text-white px-3 py-1 rounded" : "bg-gray-200 px-3 py-1 rounded"}
>
{cat}
</button>
))}
</div>
);
}
export default function ProductList({ products }: { products: Product[] }) {
const [selectedCategory, setSelectedCategory] = useState("All");
const [searchQuery, setSearchQuery] = useState("");
const categories = ["All", ...new Set(products.map((p) => p.category))];
const filteredProducts = products.filter((p) => {
const matchesCategory = selectedCategory === "All" || p.category === selectedCategory;
const matchesSearch = p.name.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
});
return (
<div>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search products..."
className="border rounded px-3 py-2 mb-4 w-full"
/>
<CategoryFilter
categories={categories}
selected={selectedCategory}
onSelect={setSelectedCategory}
/>
<div className="grid grid-cols-3 gap-4">
{filteredProducts.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
);
}The Problem Without React Compiler
Without the compiler, every keystroke in the search input causes:
ProductListre-renders (state changed)categoriesarray is recreated (new reference every render)CategoryFilterre-renders (newcategoriesprop reference)onSelectcallback is recreated (new function reference)- All
ProductCardcomponents re-render
The Old Manual Fix
Before React Compiler, you would manually add:
// Manually memoizing everything — tedious and error-prone
const categories = useMemo(
() => ["All", ...new Set(products.map((p) => p.category))],
[products]
);
const filteredProducts = useMemo(
() => products.filter((p) => { /* ... */ }),
[products, selectedCategory, searchQuery]
);
const handleSelect = useCallback(
(cat: string) => setSelectedCategory(cat),
[]
);
// Plus wrapping child components in React.memo()
const ProductCard = React.memo(function ProductCard({ product }: { product: Product }) {
// ...
});What React Compiler Does Automatically
With React Compiler enabled, the original clean code (no useMemo/useCallback) is automatically transformed at build time. The compiler:
- Memoizes the
categoriescomputation - Memoizes the
filteredProductsfiltering - Memoizes the
onSelectcallback - Caches JSX elements that haven't changed
- Skips re-rendering
CategoryFilterwhen onlysearchQuerychanges
You write clean code. The compiler handles performance.
Step 6: Verifying Compiler Output
To confirm the compiler is working, install React DevTools and look for the "Memo" badge:
# In your browser, install React DevTools extension
# Then open DevTools → Components tabComponents optimized by React Compiler show a "Memo ✨" badge in React DevTools. This confirms the compiler has automatically memoized that component.
You can also check the compiled output directly. Add this to your next.config.ts for debugging:
const nextConfig: NextConfig = {
experimental: {
reactCompiler: {
// Log compilation results
logger: {
logEvent(filename, event) {
console.log(`[React Compiler] ${filename}:`, event);
},
},
},
},
};Run your dev server and check the terminal output:
npm run devYou'll see logs showing which components were compiled and what optimizations were applied.
Step 7: Measuring Performance
Let's measure the actual impact. Create a performance test component:
// app/perf-test/page.tsx
"use client";
import { useState, useEffect, useRef } from "react";
function ExpensiveChild({ value }: { value: number }) {
// Simulate expensive render
const start = performance.now();
while (performance.now() - start < 2) {
// Busy wait for 2ms
}
return <div className="p-2 border rounded">{value}</div>;
}
export default function PerfTest() {
const [count, setCount] = useState(0);
const [unrelated, setUnrelated] = useState(0);
const renderCount = useRef(0);
const startTime = useRef(0);
useEffect(() => {
renderCount.current++;
});
const items = Array.from({ length: 100 }, (_, i) => i);
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">Performance Test</h1>
<div className="flex gap-4 mb-4">
<button
onClick={() => {
startTime.current = performance.now();
setCount((c) => c + 1);
}}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Increment Related: {count}
</button>
<button
onClick={() => {
startTime.current = performance.now();
setUnrelated((u) => u + 1);
}}
className="bg-gray-500 text-white px-4 py-2 rounded"
>
Increment Unrelated: {unrelated}
</button>
</div>
<p>Render count: {renderCount.current}</p>
<div className="grid grid-cols-10 gap-2">
{items.map((i) => (
<ExpensiveChild key={i} value={i + count} />
))}
</div>
</div>
);
}Without React Compiler: Clicking "Increment Unrelated" re-renders all 100 ExpensiveChild components (about 200ms delay).
With React Compiler: Clicking "Increment Unrelated" skips all ExpensiveChild re-renders because their props haven't changed (near-instant response).
Use Chrome DevTools Performance tab to record and compare:
- Open DevTools, go to Performance tab
- Click Record
- Click "Increment Unrelated" 5 times
- Stop recording
- Compare flame charts with and without the compiler
Step 8: Handling Edge Cases
Opting Out of Compilation
If a component doesn't work correctly after compilation, you can opt it out:
// Add "use no memo" directive to skip compilation
"use no memo";
export default function LegacyComponent() {
// This component won't be compiled
// Useful for components that rely on referential identity
}Opting In (Annotation Mode)
If you're using compilationMode: "annotation", opt in specific components:
// Add "use memo" directive to enable compilation
"use memo";
export default function OptimizedComponent() {
// Only this component gets compiled
}Common Violations
The compiler will skip components that violate the Rules of React. Common issues:
1. Mutating props or state directly:
// BAD — compiler cannot optimize this
function BadComponent({ items }: { items: string[] }) {
items.sort(); // Mutating props!
return <ul>{items.map((item) => <li key={item}>{item}</li>)}</ul>;
}
// GOOD — create a new array
function GoodComponent({ items }: { items: string[] }) {
const sorted = [...items].sort();
return <ul>{sorted.map((item) => <li key={item}>{item}</li>)}</ul>;
}2. Calling hooks conditionally:
// BAD — hooks must be called unconditionally
function BadComponent({ show }: { show: boolean }) {
if (show) {
const [value, setValue] = useState(0); // Conditional hook!
}
}
// GOOD — always call hooks at top level
function GoodComponent({ show }: { show: boolean }) {
const [value, setValue] = useState(0);
if (!show) return null;
return <div>{value}</div>;
}3. Using refs during render:
// BAD — reading refs during render breaks memoization
function BadComponent() {
const ref = useRef(0);
ref.current++; // Side effect during render!
return <div>{ref.current}</div>;
}
// GOOD — use refs in effects or event handlers
function GoodComponent() {
const ref = useRef(0);
useEffect(() => {
ref.current++;
});
return <div>Check console</div>;
}Step 9: Migrating an Existing Project
For existing projects, follow this gradual migration strategy:
Phase 1: Add ESLint Plugin First
npm install -D eslint-plugin-react-compilerRun the linter and fix all violations before enabling the compiler. This ensures your code follows the Rules of React.
Phase 2: Enable in Annotation Mode
// next.config.ts
const nextConfig: NextConfig = {
experimental: {
reactCompiler: {
compilationMode: "annotation",
},
},
};Add "use memo" to a few components and test thoroughly.
Phase 3: Switch to Infer Mode
Once you're confident, switch to the default mode:
const nextConfig: NextConfig = {
experimental: {
reactCompiler: true,
},
};Phase 4: Remove Manual Memoization
Now you can safely remove manual useMemo, useCallback, and React.memo calls:
# Find all manual memoization in your codebase
grep -rn "useMemo\|useCallback\|React.memo" --include="*.tsx" --include="*.ts" src/Remove them one by one, verifying the compiler handles each case correctly.
Step 10: Best Practices
Do
- Write clean, idiomatic React — the compiler rewards simple code
- Follow the Rules of React — pure components, no side effects during render
- Use the ESLint plugin — catch violations before they reach production
- Test performance with DevTools — verify the compiler is helping
Don't
- Don't keep manual memoization alongside the compiler — it's redundant
- Don't use
"use no memo"as a first resort — fix the underlying issue instead - Don't rely on referential identity for business logic — the compiler may change when objects are created
- Don't mutate objects or arrays in place — always create new references
Server Components
React Compiler primarily benefits Client Components ("use client"). Server Components already render once on the server, so memoization has less impact. Focus your optimization efforts on interactive client-side code.
Troubleshooting
Build Errors After Enabling Compiler
If your build fails, check the error message. Common causes:
- Syntax not supported — ensure you're using standard React patterns
- Third-party library conflict — some libraries use non-standard patterns; use
"use no memo"on wrapper components - Outdated React version — upgrade to React 19+
Component Behaves Differently
If a component works differently after compilation:
- Add
"use no memo"to isolate the issue - Check if the component relies on referential identity
- Verify it follows the Rules of React
- Report the issue to the React Compiler GitHub repository
No Performance Improvement
If you don't see improvements:
- Your components may already be efficient
- The bottleneck might be network or data fetching, not rendering
- Use React DevTools Profiler to identify the actual bottleneck
Conclusion
React Compiler transforms how we write React applications. Instead of littering code with useMemo, useCallback, and React.memo, you write clean, simple components and let the compiler handle optimization automatically.
Key takeaways:
- One line in
next.config.tsenables automatic optimization - ESLint plugin catches issues before they reach production
- Clean code wins — the compiler rewards idiomatic React
- Gradual adoption is supported through annotation mode
- Performance gains are real — especially for complex UIs with frequent re-renders
The future of React performance is automatic. Start using React Compiler today and let your code be both clean and fast.
Next Steps
- Read the official React Compiler documentation for the latest updates
- Explore the React DevTools Profiler for performance analysis
- Check out our React 19 Server Actions tutorial for more React 19 features
- Learn about Next.js 15 Partial Prerendering for server-side optimization
Discuss Your Project with Us
We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.
Let's find the best solutions for your needs.
Related Articles

Next.js 15 Partial Prerendering (PPR): Build a Blazing-Fast Dashboard with Hybrid Rendering
Master Next.js 15 Partial Prerendering (PPR) — combine static and dynamic rendering in a single page. Build an analytics dashboard with instant static shells and streaming dynamic content.

Build a Real-Time Full-Stack App with Convex and Next.js 15
Learn how to build a real-time full-stack application using Convex and Next.js 15. This tutorial covers schema design, queries, mutations, real-time subscriptions, authentication, and file uploads — all with end-to-end type safety.

Build a Full-Stack App with Firebase and Next.js 15: Auth, Firestore & Real-Time
Learn how to build a full-stack app with Next.js 15 and Firebase. This guide covers authentication, Firestore, real-time updates, Server Actions, and deployment to Vercel.