React Compiler: What It Does and Why It Matters
Learn how React Compiler automatically optimizes your components by eliminating manual memoization, what it transforms under the hood, and how to adopt it.
Tags
React Compiler: What It Does and Why It Matters
React Compiler is a build-time tool that automatically optimizes your React components by analyzing their code and inserting fine-grained memoization. It eliminates the need for manual useMemo, useCallback, and React.memo by understanding which values actually change between renders and caching everything else. Previously known internally as "React Forget," the compiler reached its v1.0 release and represents a fundamental shift from runtime performance tricks to compile-time optimization.
TL;DR
React Compiler analyzes your component code at build time and automatically inserts memoization where needed. It understands reactive dependencies, tracks value changes, and ensures components only re-render the parts that actually need updating. This makes useMemo, useCallback, and React.memo unnecessary for most use cases. You enable it through your build configuration, and it works with idiomatic React code that follows the Rules of React.
Why This Matters
Manual memoization has been one of React's biggest developer experience pain points. Every React developer has faced the question: should this value be wrapped in useMemo? Should this callback use useCallback? Should this component be wrapped in React.memo? Getting it wrong in either direction has consequences — too little memoization causes unnecessary re-renders, while too much adds cognitive overhead and can actually hurt performance if the memoization cost exceeds the re-render cost.
The problem compounds in teams. Code reviews become debates about memoization strategy. Junior developers either over-memoize everything or skip it entirely. Dependency arrays become a source of subtle bugs when developers forget to include a dependency or include one that changes referential identity on every render.
React Compiler solves this systematically. Instead of relying on developers to manually identify and implement memoization, the compiler performs a static analysis of your code and determines exactly what needs to be cached. It does this more precisely than any human could, because it tracks every variable, every expression, and every dependency through the entire component.
For teams, this means less time debating memoization, fewer performance-related bugs, and a cleaner codebase without hooks that exist solely for optimization.
How It Works
Static Analysis and Reactive Tracking
React Compiler operates as a Babel plugin that transforms your component code during the build step. It parses each component and hook, builds an internal representation of data flow, and identifies which values are "reactive" — meaning they can change between renders.
Consider this component before compilation:
function ProductCard({ product, onAddToCart }: ProductCardProps) {
const discountedPrice = product.price * (1 - product.discount);
const formattedPrice = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(discountedPrice);
const handleClick = () => {
onAddToCart(product.id, discountedPrice);
};
return (
<div className="product-card">
<h3>{product.name}</h3>
<span>{formattedPrice}</span>
<button onClick={handleClick}>Add to Cart</button>
</div>
);
}Without the compiler, every time the parent re-renders, this component recalculates discountedPrice, recreates the Intl.NumberFormat instance, formats the price, and creates a new handleClick function — even if product and onAddToCart have not changed.
The compiler analyzes this and produces output conceptually equivalent to:
function ProductCard({ product, onAddToCart }: ProductCardProps) {
const $ = useMemoCache(4);
let discountedPrice;
if ($[0] !== product.price || $[1] !== product.discount) {
discountedPrice = product.price * (1 - product.discount);
$[0] = product.price;
$[1] = product.discount;
} else {
discountedPrice = $[2];
}
$[2] = discountedPrice;
// formattedPrice, handleClick, and JSX are similarly cached
// based on their actual dependencies
// ...
}The compiler creates a memoization cache (useMemoCache) and wraps each computation in a conditional check against its actual dependencies. This is more granular than what useMemo provides — the compiler can cache individual expressions, not just the values you manually decide to memoize.
What the Compiler Eliminates
The compiler makes several common patterns unnecessary:
useMemo for computed values:
// Before: manual memoization
function OrderSummary({ items }: { items: OrderItem[] }) {
const total = useMemo(
() => items.reduce((sum, item) => sum + item.price * item.quantity, 0),
[items]
);
const taxAmount = useMemo(() => total * 0.08, [total]);
return (
<div>
<p>Subtotal: ${total.toFixed(2)}</p>
<p>Tax: ${taxAmount.toFixed(2)}</p>
</div>
);
}
// After: just write the computation naturally
function OrderSummary({ items }: { items: OrderItem[] }) {
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const taxAmount = total * 0.08;
return (
<div>
<p>Subtotal: ${total.toFixed(2)}</p>
<p>Tax: ${taxAmount.toFixed(2)}</p>
</div>
);
}useCallback for event handlers:
// Before: wrapping every handler
function SearchBar({ onSearch }: { onSearch: (query: string) => void }) {
const [query, setQuery] = useState("");
const handleSubmit = useCallback(
(e: FormEvent) => {
e.preventDefault();
onSearch(query);
},
[onSearch, query]
);
const handleClear = useCallback(() => {
setQuery("");
onSearch("");
}, [onSearch]);
return (
<form onSubmit={handleSubmit}>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button type="button" onClick={handleClear}>Clear</button>
<button type="submit">Search</button>
</form>
);
}
// After: just write normal functions
function SearchBar({ onSearch }: { onSearch: (query: string) => void }) {
const [query, setQuery] = useState("");
function handleSubmit(e: FormEvent) {
e.preventDefault();
onSearch(query);
}
function handleClear() {
setQuery("");
onSearch("");
}
return (
<form onSubmit={handleSubmit}>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button type="button" onClick={handleClear}>Clear</button>
<button type="submit">Search</button>
</form>
);
}React.memo for component wrappers:
// Before: wrapping components to prevent re-renders
const ExpensiveChart = React.memo(function ExpensiveChart({
data,
config,
}: ChartProps) {
// complex rendering logic
return <canvas ref={canvasRef} />;
});
// After: the compiler handles it — just export the component directly
function ExpensiveChart({ data, config }: ChartProps) {
// complex rendering logic
return <canvas ref={canvasRef} />;
}The Rules of React
The compiler relies on your code following the Rules of React. These are not new rules — they are the same rules that have always existed but were previously only enforced by the linter:
- ›
Components must be pure during render. Given the same props and state, a component must return the same JSX. No reading from mutable external variables during render.
- ›
No side effects during render. Do not modify DOM, write to external stores, or make network requests during the render phase. Use
useEffectfor side effects. - ›
Props and state are immutable. Never mutate props or state directly. Always create new objects or arrays.
// This breaks the compiler — mutating during render
function BadComponent({ items }: { items: Item[] }) {
items.sort((a, b) => a.name.localeCompare(b.name)); // Mutates the prop!
return <List items={items} />;
}
// This works — creating a new sorted array
function GoodComponent({ items }: { items: Item[] }) {
const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
return <List items={sorted} />;
}The compiler includes a strict mode that validates these rules at compile time. If it detects code that violates them, it will skip optimization for that component and emit a warning.
Practical Implementation
Enabling the Compiler
In Next.js 15+, enabling the compiler is a single configuration change:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
reactCompiler: true,
},
};
export default nextConfig;For Vite or other bundlers, add the Babel plugin:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [
react({
babel: {
plugins: [["babel-plugin-react-compiler"]],
},
}),
],
});Opting Out Specific Files
If the compiler causes issues with a specific file (perhaps due to unconventional patterns), you can opt it out:
"use no memo";
// This file will not be compiled
function LegacyComponent() {
// ... code that doesn't follow Rules of React
}Validating Your Codebase
Before enabling the compiler, use the ESLint plugin to check your code for violations:
npm install 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 will flag any code patterns that would prevent the compiler from optimizing correctly.
Common Pitfalls
Assuming the compiler fixes bad patterns. The compiler optimizes idiomatic React code. If your component mutates props, reads from mutable globals during render, or has other rule violations, the compiler will skip it. Fix the underlying issue first.
Removing useMemo/useCallback too eagerly before enabling the compiler. If you strip manual memoization before turning on the compiler, you may see performance regressions in the interim. Enable the compiler first, verify it works, then gradually remove manual memoization.
Not testing after enabling the compiler. While the compiler is designed to be behavior-preserving, edge cases exist. Run your test suite and verify critical user flows after enabling it.
Expecting the compiler to optimize third-party library code. The compiler only transforms your source code. If a third-party component causes excessive re-renders, the compiler cannot fix that unless you control the code that passes props to it.
Ignoring compiler warnings. If the compiler emits warnings about skipped components, investigate them. They often reveal bugs or anti-patterns that would cause issues regardless of the compiler.
When to Use (and When Not To)
Use React Compiler when:
- ›You are starting a new project on React 19 or later
- ›Your codebase follows the Rules of React (pure components, immutable updates)
- ›You want to reduce cognitive overhead around memoization decisions
- ›You are migrating from a codebase heavy with manual memoization
Wait to adopt when:
- ›Your codebase has significant rule violations that would take time to fix
- ›You rely heavily on patterns that mutate state or props during render
- ›You are on a version of React older than 19 and cannot upgrade yet
The compiler will not help when:
- ›Performance issues stem from unnecessary network requests or poor data architecture
- ›The bottleneck is in third-party libraries you do not control
- ›You need to optimize non-React code like heavy canvas rendering or WebGL
FAQ
Does React Compiler replace useMemo and useCallback?
Yes. React Compiler analyzes dependencies and reactivity at build time and automatically inserts equivalent memoization. You no longer need to manually wrap values in useMemo or functions in useCallback — the compiler handles it for you.
Is React Compiler opt-in or opt-out?
React Compiler is opt-in at the configuration level. You enable it in your build config (Babel plugin or Next.js config). Once enabled, it compiles all components by default, but you can opt out individual files or directories using the "use no memo" directive.
Does React Compiler work with existing codebases?
Yes. The compiler is designed to work with idiomatic React code. Existing useMemo and useCallback calls are safe to keep — the compiler will optimize around them. You can gradually remove manual memoization after enabling the compiler.
What are the requirements to use React Compiler?
React Compiler requires React 19 or later as the runtime. It works with Next.js, Vite, and other build tools through its Babel plugin. Your code must follow the Rules of React — pure render functions and no side effects during render.
Collaboration
Need help with a project?
Let's Build It
I help startups and established companies design, build, and scale world-class digital products. From deep technical architecture to pixel-perfect UI — let's bring your vision to life.
Related Articles
How to Design API Contracts Between Micro-Frontends and BFFs
Learn how to design stable API contracts between Micro-Frontends and Backend-for-Frontend layers with versioning, ownership boundaries, error handling, and schema governance.
Next.js BFF Architecture
An architectural deep dive into using Next.js as a Backend-for-Frontend, including route handlers, server components, auth boundaries, caching, and service orchestration.
Next.js Cache Components and PPR in Real Apps
A practical guide to using Next.js Cache Components and Partial Prerendering in real applications, with tradeoffs, cache strategy, and freshness considerations.