React performance work has traditionally meant teaching every developer when to reach for useMemo, useCallback, and React.memo. That approach can work, but it spreads a difficult, error-prone optimisation discipline throughout the codebase. React Compiler changes that trade-off by automatically memoizing components and values when it can prove doing so is safe.
For a product team, the important question is not whether the compiler is clever. It is whether it can be introduced without turning a mature React application into a risky rewrite. The answer is yes, provided the rollout is treated as an incremental engineering change rather than a switch to flip everywhere.
TL;DR: React Compiler can remove much of the manual memoization burden, but it is not a substitute for good component boundaries, correct Hook usage, or measurement. Start with a small surface area, make linting and observability part of the rollout, and delete manual memoization only after the compiler is demonstrably helping.
Table of contents
- What React Compiler actually changes
- Why existing apps are the hard case
- A safe rollout sequence
- Code patterns to review before enabling it
- How to measure whether it helped
- What not to expect from the compiler
- A sensible team policy
- FAQ
What React Compiler actually changes
React Compiler is a build-time optimisation tool. It analyses React components and Hooks, then inserts the equivalent of memoization where it can safely preserve React’s semantics. In practical terms, it aims to prevent avoidable re-renders and repeated calculations without requiring a developer to wrap everything in useMemo, useCallback, or React.memo.
That last point matters. Manual memoization is not free:
- Dependencies are easy to get wrong.
- Memoized callbacks can obscure straightforward code.
- A memo may retain memory longer than the calculation it avoids.
- Reviewing whether a memo is necessary is surprisingly expensive.
- Teams often cargo-cult memoization into places where it has no measurable benefit.
The compiler is not simply a global React.memo. It reasons about values and component execution, and it relies on code following the Rules of React. When a component is pure with respect to its inputs and Hooks are used correctly, the compiler has enough structure to optimise safely.
The official React documentation is worth reading before a rollout, especially its pages on installation, incremental adoption, and debugging. Those pages make the right expectation clear: adoption can be gradual, and unsupported code should be treated as a migration signal, not an excuse to disable guardrails.
Why existing apps are the hard case
A new application can establish compiler-friendly conventions from day one. An established application has history: custom Hooks with ambiguous ownership, mutable module state, legacy third-party code, and performance work that was written for a different rendering model.
The risk is usually not that enabling the compiler makes a correct component incorrect. The risk is discovering that code which appeared to work was relying on an invalid pattern, hidden mutation, or stale closure.
Consider a common anti-pattern:
const filters = { status: selectedStatus };
function ProductList({ products }: { products: Product[] }) {
useEffect(() => {
filters.status = selectedStatus;
}, [selectedStatus]);
return products.filter(product => product.status === filters.status);
}The value read during render is coupled to mutable external state. This is difficult for humans to reason about and unsafe for an optimiser to assume is stable. The fix is not compiler-specific. Make render inputs explicit:
function ProductList({
products,
selectedStatus,
}: {
products: Product[];
selectedStatus: string;
}) {
const visibleProducts = products.filter(
product => product.status === selectedStatus,
);
return <Results products={visibleProducts} />;
}That code is easier to test, easier to review, and gives the compiler a clean model of the component.
A safe rollout sequence
1. Establish a baseline before changing anything
Choose two or three real workflows where render cost matters. Examples include a filterable table, a dashboard with live data, and a complex form. Record useful measures before enabling the compiler:
- interaction latency for filtering, typing, or navigation;
- number and duration of commits in React DevTools Profiler;
- client-side error rate;
- bundle size and build time;
- a small set of user-facing performance metrics such as INP.
Do not start from a synthetic “memoization count.” The outcome is a faster and more reliable interaction, not a codebase containing fewer Hook calls.
2. Upgrade React and turn on the lint rules
Use a React version supported by the compiler and add the recommended compiler and lint configuration for your build stack. The linter is part of the product, not optional polish. It catches code that prevents safe optimisation and makes migrations visible in pull requests.
At this stage, fix obvious Rule-of-Hooks violations and render-time side effects. Avoid combining this work with a broad visual redesign or dependency upgrade. A narrow change is easier to roll back and easier to diagnose.
3. Compile a small, owned route first
Start with a feature that your team owns end-to-end. It should have automated tests, realistic traffic or staging coverage, and enough component complexity to show a result. Do not begin with the shared design system, authentication flow, or the page with the largest revenue impact.
If your configuration supports targeting a directory or package, compile only that scope initially. Run unit tests, end-to-end tests, and manual checks on the chosen workflow. Compare the profiler trace with the baseline.
4. Use directives sparingly
React Compiler supports function-level directives that can control compilation. They are useful escape hatches during a staged rollout, but they should not become permanent decoration across the app.
A temporary opt-out can protect a complex legacy boundary while work is planned:
function LegacyCheckoutSummary() {
"use no memo";
// Existing code with a known migration issue.
return <CheckoutSummary />;
}Treat every opt-out as a ticket with an owner and expiry expectation. If no one owns it, it will silently become architecture.
5. Expand by domain, then remove redundant code
Once the first route is stable, move to adjacent routes or a feature package. Only after repeatable evidence should you remove redundant manual useMemo, useCallback, or React.memo calls. Removing them immediately creates unnecessary churn and makes regressions harder to attribute.
When you do remove them, keep the change focused. The best migration pull requests explain which profiler trace or user interaction motivated the cleanup.
Code patterns to review before enabling it
The compiler rewards the same habits that already make React code robust.
Keep render pure
Rendering should calculate UI from props, state, and stable context, not write to external objects, mutate props, or trigger requests. Effects are the place for synchronization with systems outside React.
Bad:
function SearchPage({ query }: { query: string }) {
analytics.lastQuery = query;
return <SearchResults query={query} />;
}Better:
function SearchPage({ query }: { query: string }) {
useEffect(() => {
analytics.track("search_viewed", { query });
}, [query]);
return <SearchResults query={query} />;
}Do not mutate inputs
Props, Hook return values, and context values should be treated as immutable. Mutation may look efficient locally, but it makes update behavior dependent on timing and object identity.
Make dependencies honest
An effect with a missing dependency might appear to reduce work, but it is actually an incorrect subscription to state. Fix the dependency model before trying to improve performance. The compiler cannot compensate for stale closures.
Keep custom Hooks conventional
Custom Hooks should call Hooks unconditionally and expose a predictable contract. They are a powerful place to centralize stateful behavior, but they can also conceal invalid patterns. A compiler rollout is an excellent moment to audit the most widely used custom Hooks.
How to measure whether it helped
The compiler can reduce work that users never notice, while a slow network request or oversized image continues to dominate the experience. Measure at three layers.
- User interaction: Is typing, filtering, dragging, or switching tabs visibly more responsive? Track INP and route-level interaction timings where possible.
- React render work: Use React DevTools Profiler to compare commit durations and component render frequency on identical scenarios.
- Operational impact: Monitor build duration, client errors, and CPU use on lower-end test devices. A change that speeds one desktop trace but makes builds unusable is not a net win.
For example, profile a large filterable list before and after compilation. Keep the same data size and input sequence. If the list still feels slow, inspect the actual bottleneck. It may be layout work, expensive chart rendering, or an API request, none of which automatic memoization solves by itself.
Performance work should follow evidence. React Compiler makes one class of optimisation more reliable; it does not make performance measurement optional.
What not to expect from the compiler
React Compiler is not a replacement for architecture. It will not:
- split an oversized route bundle;
- make a slow database query fast;
- fix waterfall data fetching;
- eliminate expensive DOM layout or image decoding;
- repair invalid state modelling;
- choose sensible cache boundaries for server data.
It also does not mean every old memo is harmful. A library API may intentionally require referential stability, and a calculation may be used outside a compiler-managed component boundary. Keep a reason for exceptions, but do not preserve them just because they are familiar.
For teams using Next.js or another full-stack React framework, the rollout should coexist with server-rendering and caching strategy. Compiler optimization affects client React execution. It complements, rather than replaces, work such as reducing client components, streaming useful UI early, and choosing cache lifetimes deliberately.
A sensible team policy
A practical policy is simple:
- Enable the compiler in new, owned feature code first.
- Require compiler lint warnings to be understood, not suppressed by default.
- Avoid adding manual memoization without a measured reason.
- Track opt-outs and revisit them each quarter.
- Use profiler evidence when deleting existing manual memoization.
- Keep performance budgets at the user-interaction level.
This changes the review conversation from “should we wrap this callback?” to “what user-visible work are we avoiding?” That is a healthier question. It also lets experienced React developers spend less time maintaining dependency arrays and more time improving data flow, accessibility, and product behavior.
React Compiler is best understood as a maintenance tool as much as a performance tool. Its immediate benefit may be a smoother complex screen. Its longer-term benefit is making the default style of React code clearer, more declarative, and less littered with defensive optimization.
FAQ
Does React Compiler mean useMemo and useCallback are obsolete?
Not instantly. The compiler can automate many common cases, but existing code should be migrated incrementally. Keep explicit memoization where it has a documented reason, then remove it only after testing and profiling.
Can React Compiler be enabled gradually?
Yes. React’s documentation supports incremental adoption, and a route- or package-based rollout is the safest approach for an established application.
Will React Compiler fix slow React apps automatically?
No. It can reduce unnecessary React work, but data fetching, bundle size, image loading, layout, and third-party scripts may still be the dominant bottleneck.
What is the biggest migration risk?
Code that relies on mutation, render-time side effects, or invalid Hook behavior. These are worth fixing regardless of whether the compiler is enabled.
Should a team remove all manual memoization after enabling it?
No. Leave existing code in place initially, validate behavior and performance, then remove redundant memoization in focused, measurable changes.