Say you are trying to decide whether to rip out a component tree full of useMemo and useCallback calls that a previous team member added defensively, one hook at a time, without ever profiling whether any of it mattered. You’ve read that React Compiler can do this work automatically, but “automatic memoization” is a vague enough phrase that it’s hard to know what it actually changes about how you write code, or whether it’s safe to trust with an existing codebase. This walkthrough goes through the mechanics step by step, from what problem the compiler solves to how you verify it’s doing its job correctly.
Step 1: Understand the Problem the Compiler Is Solving
Before the compiler, keeping a component tree fast required developers to manually identify which values and functions were causing unnecessary re-renders, then wrap those specific values in useMemo or useCallback, and pass the wrapped versions down consistently. Miss one dependency, forget to wrap a callback passed to a memoized child, or get a dependency array wrong, and the whole optimization silently stops working while the code still looks correct.
This manual process doesn’t scale well across a large team. Some developers over-memoize out of caution, adding overhead to simple components that never needed it. Others under-memoize because tracking every prop reference by hand is tedious and error-prone. React Compiler exists to remove this decision from the developer entirely by analyzing the component at build time and inserting the equivalent memoization automatically, based on data-flow analysis rather than guesswork.
Step 2: Understand What the Compiler Actually Does
The compiler runs as a build step, most commonly via a Babel plugin, and analyzes your component and hook functions to determine which values depend on which inputs. Where it can prove that a value or function reference would otherwise be recreated unnecessarily on a re-render, it inserts caching logic comparable to what you’d have written by hand with useMemo or useCallback — except this analysis considers the entire component, not just the values you remembered to wrap.
Here’s a simple before-and-after to make this concrete. This is what a component might look like written manually, with memoization added defensively:
function ProductCard({ product, onAddToCart }) {
const formattedPrice = useMemo(
() => formatCurrency(product.price),
[product.price]
);
const handleClick = useCallback(() => {
onAddToCart(product.id);
}, [product.id, onAddToCart]);
return (
<div className="card">
<span>{formattedPrice}</span>
<button onClick={handleClick}>Add to Cart</button>
</div>
);
}
With the compiler enabled, you can write the plain version and let the build step handle the caching:
function ProductCard({ product, onAddToCart }) {
const formattedPrice = formatCurrency(product.price);
const handleClick = () => {
onAddToCart(product.id);
};
return (
<div className="card">
<span>{formattedPrice}</span>
<button onClick={handleClick}>Add to Cart</button>
</div>
);
}
The compiled output that ships to the browser is functionally equivalent to the manual version above — the compiler generates that caching logic for you during the build, so the source stays readable while the runtime behavior stays optimized.
Step 3: Set Up the Compiler and ESLint Plugin in Your Project
Adoption starts with two pieces: the Babel plugin that performs the transformation, and the companion ESLint plugin (eslint-plugin-react-compiler) that flags code the compiler cannot safely optimize. Installing only the Babel plugin without the linter is a common mistake — without lint feedback, you won’t know when a component silently falls outside the compiler’s ability to optimize it.
A typical setup adds the plugin to your Babel config:
// babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', { target: '19' }],
],
};
And adds the corresponding rule set to your ESLint config so violations surface during development rather than being discovered later through a profiler session:
// eslint.config.js
import reactCompiler from 'eslint-plugin-react-compiler';
export default [
{
plugins: { 'react-compiler': reactCompiler },
rules: {
'react-compiler/react-compiler': 'error',
},
},
];
Frameworks built on top of React, including Next.js, typically expose this as a configuration flag rather than requiring you to touch the Babel config directly, but the underlying mechanism is the same.
Step 4: Understand the Rules of React the Compiler Depends On
The compiler’s analysis relies on your components following the Rules of React — no mutating props or state directly, no calling hooks conditionally, treating data as immutable unless it flows through the recognized update mechanisms (useState, useReducer, and so on). This isn’t a new set of rules invented for the compiler; it’s the same set of rules React has always asked developers to follow, except now violating them has a more direct cost, since the compiler may skip optimizing a component it can’t safely reason about.
A component that mutates an object prop in place, for example, breaks the assumptions the compiler relies on to detect when a value has changed:
// The compiler cannot safely optimize this
function BadExample({ items }) {
items.push({ id: 'new' }); // Mutating a prop directly
return <List items={items} />;
}
This is exactly the kind of pattern the ESLint plugin is designed to catch before it reaches a build, since silent opt-outs are far harder to diagnose after the fact.
Step 5: Verify the Compiler Is Actually Optimizing Your Components
Adding the plugin to your build doesn’t guarantee every component benefits from it — some will still be skipped due to bailouts, and it’s worth confirming which ones. React DevTools shows a badge on components that have been compiled, which gives you a direct, visual way to check coverage across your tree rather than assuming the plugin worked everywhere it was applied.
For components you suspect should be optimized but aren’t showing the badge, checking the console output from the ESLint plugin or the compiler’s build logs (depending on your setup) will usually surface the specific pattern causing the bailout — often a mutation, a conditional hook call, or a ref accessed during render.
Step 6: Decide Where Manual Memoization Is Still Worth Keeping
The compiler removes the need for most defensive useMemo and useCallback calls, but it doesn’t eliminate every reason to memoize manually. Expensive computations that depend on values the compiler can’t statically analyze — data fetched asynchronously and transformed in complex ways, for instance — may still benefit from explicit memoization if profiling shows the compiler isn’t catching that specific case.
The practical approach is to remove existing manual memoization incrementally rather than all at once, profiling before and after with React DevTools to confirm render counts and timings haven’t regressed. Trusting the compiler blindly across an entire large codebase in one pass makes it much harder to isolate the cause if a specific component’s performance changes unexpectedly.
Step 7: Roll Out Gradually and Monitor for Regressions
Because the compiler can be applied selectively — file by file, or directory by directory, depending on your build configuration — a staged rollout is safer than flipping it on for an entire application at once. Start with a section of the app that has clear performance baselines already established, confirm the compiled version matches or beats those numbers, and expand from there.
Below is a quick reference for the state of a component before and after this migration, which is useful to keep in mind when deciding where to start:
| Aspect | Before Compiler | After Compiler |
|---|---|---|
| Memoization | Manual useMemo/useCallback calls |
Inserted automatically at build time |
| Risk of stale dependencies | Present if dependency arrays are wrong | Eliminated, since the compiler derives dependencies itself |
| Code readability | Cluttered with optimization hooks | Closer to plain component logic |
| Requirement | None beyond hook usage | Strict adherence to the Rules of React |
| Verification method | Manual profiling | DevTools compiled-component badge plus profiling |
What This Changes About Writing React Components
Once the compiler is in place and verified across a codebase, the practical effect is that memoization becomes an implementation detail handled by tooling rather than a manual discipline every contributor needs to maintain correctly. The code that remains reads closer to what React looked like before performance concerns forced developers to reach for useMemo on every other line — which was the original goal of the API in the first place, before manual optimization became a habit applied everywhere out of caution.
Have you started migrating a project to React Compiler, or are you evaluating whether your codebase’s patterns are compatible with it? Share what bailouts or lint errors you’re running into, and we can work through whether they point to a real problem or just a pattern the compiler needs a small adjustment to understand.
🔗 Recommended Reading
- Measuring Core Web Vitals in React Apps: Myth vs. Reality
- Performant React Animations: A Step-by-Step Guide to Choosing CSS vs. JavaScript
- Diagnosing UI Freezes in React: Your Guide to Web Workers
- Are Your React Dependencies Secretly Slowing You Down?
- How We Cut Our React App's Image Load Time by 70%: A Case Study