What React Compiler Optimizes and What It Leaves on Your Plate
Understand which performance work React Compiler automates, why slow APIs and heavy bundles stay your job, and how to adopt it safely in an existing React codebase.
This article is published in English.
React Compiler removes a lot of manual memoization, which makes it tempting to treat it as a cure for slow apps. It is not one: it optimizes rendering work that React controls, and nothing else. Below you will see where that boundary sits, which slowdowns still need an engineer, and how to enable the compiler in an existing codebase without regressions.
From a memoization checklist to automatic memoization
For a long time, React performance advice came down to a short list of tools:
useMemo()
useCallback()
React.memo()
A component re-rendered too often, so you wrapped it. A calculation looked costly, so you cached it. A callback went to a child, so you stabilized it. Every answer was manual memoization, with a dependency array to maintain.
React Compiler changes that default. Version 1.0 is marked stable, and the React team's guidance for new code is to let the compiler handle memoization instead of sprinkling useMemo and useCallback by hand. Check the current React docs for the latest adoption advice.
The catch fits in one sentence: the compiler can make React's own work cheaper, but it has no influence over work that happens outside React.
What the compiler actually does
At build time, the compiler analyzes components and hooks and inserts memoization where its analysis shows it pays off. Take a component that filters a product list before rendering it:
function ProductList({ products }) {
const visible = products.filter(
product => product.inStock
);
The filtered array then goes straight into a child component:
return <List items={visible} />;
}
Before the compiler, a performance-minded developer would often cache the filter explicitly so List receives the same array reference until products changes:
const visible = useMemo(
() => products.filter(
product => product.inStock
),
[products]
);
With the compiler enabled, you usually skip that code. It works out the dependencies itself, can memoize more precisely than hand-written hooks typically do, and does not rely on you listing dependencies correctly. React calls this automatic memoization for components and hooks.
Notice the scope in that description, though. Every item on the list is a React construct.
A mental model: a rendering optimizer
It helps to picture the compiler as something that sits over two kinds of React work and trims both:
React Compiler
│
┌───────────┴───────────┐
▼ ▼
Component work Hook calculations
│ │
▼ ▼
memoization memoization
│ │
└───────────┬───────────┘
▼
fewer unnecessary
React updates
Fewer unnecessary re-renders can be a real win. The important part of the diagram, though, is what it leaves out.
Slow backends are outside its reach
Consider an async component that waits on an API before rendering a dashboard:
async function Dashboard() {
const data = await fetch(
"https://api.example.com/dashboard"
);
Once the data arrives, it is handed to the UI:
return <DashboardUI data={data} />;
}
If that endpoint takes 800 ms, the compiler will not bring it down to 50 ms. It can cheapen React turning data into UI, not make your backend respond sooner. As a timeline:
Browser
│
▼
React
│
▼
API request ──────────► 800ms
│
▼
Response
│
▼
React render
Memoization lives almost entirely in the last step. If most of the time is network wait, you are optimizing the wrong layer; the fixes are faster queries, caching, smaller responses or parallel fetching. For separating network time from client time, see Diagnosing React Performance Issues Beyond the API Response Time.
JavaScript you should not have shipped
The same limit applies to bundle size. Picture an entry page that pulls in a heavy charting library, a rich-text editor and an analytics SDK before anyone opens the screens that use them:
Initial page
│
├── React
├── UI
├── Chart library
├── Editor
└── Analytics SDK
The compiler removes no dependencies; every byte still gets downloaded, parsed and executed. What helps is code splitting, for example loading the analytics screen on demand:
const Analytics = lazy(
() => import("./Analytics")
);
Now that code is fetched only when the feature is actually rendered, and a surrounding Suspense boundary shows a fallback while it loads. This is a bundle architecture decision, and no amount of memoization substitutes for it.
Where expectations go wrong
A common story: an application feels sluggish, someone enables the compiler, and the team expects this outcome:
Slow app
│
▼
React Compiler
│
▼
Fast app
That is too simple. Slowness has several sources, and the compiler covers one branch:
SLOW APP
│
┌───────────────┼───────────────┐
▼ ▼ ▼
React work Network Browser
│ │ │
▼ ▼ ▼
Compiler API/cache JS/rendering
│ │ │
▼ ▼ ▼
Optimized Engineer Engineer
The compiler is one tool in the performance toolbox, not the toolbox itself. Network latency and browser costs such as script evaluation and layout still need a person.
Should you delete existing useMemo calls?
Not automatically. In new code, the official default is to let the compiler memoize. For existing code, React advises leaving current memoization in place, or removing it only after careful testing. The reason is behavioral, not just performance-related: a memoized value used as an Effect dependency determines when that Effect re-runs, so changing how it is memoized can change when side effects fire.
So resist the urge to run a codebase-wide cleanup like this:
useMemo → DELETE
useCallback → DELETE
React.memo → DELETE
A safer path is gradual, with measurement at every step:
Existing app
│
▼
Enable compiler
│
▼
Run tests
│
▼
Profile
│
▼
Remove unnecessary
manual memoization
carefully
The compiler exists to make manual memoization less necessary going forward, not to declare every existing optimization wrong overnight.
The compiler as a code-quality check
A benefit that gets less attention: the compiler includes validation passes built around the Rules of React, and its lint integration can report patterns such as these:
set-state-in-render
set-state-in-effect
unsafe ref access
React notes that these diagnostics can expose bugs an app already contained. Since the compiler skips code that breaks its assumptions, the warnings also reveal which components go unoptimized, making it part of the daily feedback loop rather than just a performance plugin.
Profile first, then choose the fix
When an interface feels slow, measure before you reach for any tool, and let the measurement pick the layer:
SLOW UI
│
▼
PROFILE
│
┌────────┼────────┐
▼ ▼ ▼
React Network Browser
work │ │
│ │ │
▼ ▼ ▼
Compiler API/cache JS/render
│
▼
Targeted fix
Then match the fix to what the profiler shows:
- Unnecessary React re-renders: this is where React Compiler can help.
- A slow API: improve the endpoint or the data-fetching strategy.
- An oversized bundle: ship less JavaScript through splitting and dependency cuts.
- Costly DOM updates: rethink how you render, for example by virtualizing long lists.
- A heavy third-party script: remove it, defer it, or isolate it.
Key takeaways
The old model required telling React what to memoize; the new one lets you write correct React and trust the compiler with much of the rendering work. That raises the value of one skill: knowing where the time actually goes.
- The compiler optimizes work it understands, which means React rendering and hook calculations.
- It cannot shrink an oversized API response, speed up a database query, remove a third-party script, or compress a 5 MB image.
- It cannot rescue an architecture that creates unnecessary work everywhere.
- In existing code, enable it, test, profile, and only then remove manual memoization.
- Treat its diagnostics as free bug reports about Rules of React violations.
Ask not whether the compiler is on, but where the app spends its time; the React share of that is what the compiler is for.