In October 2025, React Compiler 1.0 was released as a stable version. It's said that we'll no longer need to manually write useMemo, useCallback, or React.memo. For those of us who have struggled with forgetting dependencies or getting them wrong, this is a welcome evolution. That said, there are some things that seem worth knowing before adopting it, so I've tried to organize them here. Some of my understanding may be incorrect, so please refer to the official documentation for accurate information.
What is React Compiler?
It's a tool that automatically inserts memoization at build time — something we previously had to write manually. It's already running in production at Meta, including Quest Store, with reported improvements of up to 12% faster initial loads and more than 2.5× faster interactions.
https://react.dev/blog/2025/10/07/react-compiler-1
Installation
React Compiler is provided as a Babel plugin.
npm install -D --save-exact babel-plugin-react-compiler@latest
For Vite, you can configure it through the babel option in vite-plugin-react:
// vite.config.ts
export default defineConfig({
plugins: [
react({
babel: {
plugins: [
['babel-plugin-react-compiler', { target: '18' }], // For React 18
],
},
}),
],
});
It works with React 17 and above. For versions below React 19, you'll need to add the react-compiler-runtime package and specify the version with the target option.
https://react.dev/learn/react-compiler/introduction
Differences from React.memo
| Aspect | React.memo / useMemo | React Compiler |
|---|---|---|
| Scope | Explicitly specified | Automatic analysis |
| Granularity | Component or value level | Down to expression level (even after conditionals) |
| Dependencies | Explicit array | Implicit |
| Custom comparison (2nd arg of React.memo) | Possible | Not possible |
| Adoption cost | None | Build configuration required |
The biggest difference is that dependency arrays are no longer needed.
Previously, we had to explicitly specify dependencies:
const filtered = useMemo(() => {
return items.filter(item => item.category === category);
}, [items, category]);
With React Compiler, you can write it like this:
const filtered = items.filter(item => item.category === category);
While this is simpler, it also means you can no longer tell from the code what's being memoized or what it depends on. This might cause some difficulties during code reviews or debugging.
Memoization After Conditional Branches
One technical advantage of React Compiler is that it can memoize even after conditional branches:
// Manual approach would violate Rules of Hooks
function Component({ data }) {
if (!data) return null;
const processed = useMemo(() => expensiveProcess(data), [data]); // NG
}
// React Compiler handles this fine
function Component({ data }) {
if (!data) return null;
const processed = expensiveProcess(data); // Automatically memoized
}
Variables after early returns are also automatically memoized, giving you more freedom in your code structure.
Patterns to Check Before Adopting
React Compiler optimizes code that follows the "Rules of React." These are the rules that must be followed for React to work correctly:
- Components and hooks are pure (same inputs return same outputs)
- No side effects during rendering
- Don't directly mutate props or state
https://react.dev/reference/rules
When the compiler detects a rule violation, it skips that component or hook and continues compiling the rest. The skipped parts will work as before, so the app won't break.
However, you might want to verify the behavior if you have code like the following:
Referencing external mutable objects
const data = globalCache[id]; // Reading external mutable state during render
If you read external mutable objects during render, the compiler may not be able to track dependencies correctly.
Mutable array operations
const sorted = items.sort((a, b) => a.id - b.id); // Mutates the original array
sort() directly modifies the original array, which may cause React's re-render detection to not work properly. It's safer to use [...items].sort() or toSorted().
Using ref values in calculations
const doubled = countRef.current * 2; // Reading ref during render
Reading ref.current during render will trigger a warning from ESLint's react-hooks/refs rule. Since ref changes are not reactive, they don't work well with memoization.
Date.now() or Math.random()
const timestamp = Date.now(); // May cause compiler to skip optimization due to impurity
These return non-deterministic values, so they get caught by the compiler's purity check. ESLint's react-hooks/purity rule will also warn about this.
Custom comparison with React.memo's second argument
// React.memo allowed controlling when to skip re-renders with the 2nd argument
const MemoizedComponent = React.memo(Component, (prevProps, nextProps) => {
// Return true to skip re-render
return prevProps.item.id === nextProps.item.id;
});
// React Compiler doesn't support this kind of custom comparison logic
With React.memo's second argument, you could compare only some props or do deep comparison. React Compiler doesn't support this control, so if you need such comparison, it seems the recommendation is to reconsider the design itself.
If problems occur, you can opt out specific components using the 'use no memo' directive:
function SuspiciousComponent() {
'use no memo';
// This component won't be compiled
}
https://react.dev/reference/react-compiler/directives/use-no-memo
Patterns That Cause Compiler to Skip Optimization
There are patterns reported in GitHub Issues and the community where the compiler skips optimization that aren't detected by ESLint rules. These may be supported in future versions, but as of version 1.0, the following patterns seem to require attention.
Using finally clauses
There are reports that using try...catch...finally or try...finally causes the component to be excluded from optimization.
// May not be optimized
async function fetchData() {
try {
const res = await fetch('/api/data');
return await res.json();
} finally {
console.log('Done');
}
}
As a workaround, extracting the finally logic into a custom hook allows the calling component to be optimized.
Conditional branches or Optional Chaining inside try...catch
There are reports that using if statements or ?. inside try...catch blocks prevents optimization.
// May not be optimized
try {
const data = await res.json();
setValue(data.user?.name ?? 'Unknown'); // Optional Chaining inside try
} catch {
console.error('Error');
}
Moving conditional branches and Optional Chaining outside the try...catch block reportedly allows optimization.
Dynamic import inside components
// May not be optimized
async function handleClick() {
const { helper } = await import('./utils');
helper();
}
Extracting the import to a function outside the component allows optimization.
These patterns are not explicitly documented in the official documentation and are based on community reports. You can verify actual behavior in the React Compiler Playground.
Should You Remove useMemo/useCallback?
According to the official documentation, for new code, the recommendation is to let the compiler handle it, while using useMemo/useCallback when the compiler's automatic decisions don't produce the intended behavior. For existing code, since removing them may change the compilation output, the recommendation is to either leave them in place or remove them only after thorough testing.
Decision Flowchart
Here's a summary of the decision flow based on the above.
Considering React Compiler
│
▼
┌───────────────────┐
│ New project? │
└─────────┬─────────┘
│
┌────────┴────────┐
│Yes │No
▼ ▼
┌─────────────┐ ┌──────────────────────────┐
│Can follow │ │Is existing codebase │
│Rules of │ │pure and immutable? │
│React? │ └────────────┬─────────────┘
└──────┬──────┘ │
│ ┌──────┴──────┐
┌────┴────┐ │Yes │No
│Yes │No ▼ ▼
▼ ▼ ┌───────────┐ ┌─────────────┐
┌──────┐ ┌──────┐ │Using │ │Gradual │
│Worth │ │Review│ │React.memo │ │adoption │
│trying│ │code │ │2nd arg? │ │or wait │
└──┬───┘ │design│ └─────┬─────┘ └─────────────┘
│ │first │ │
│ └──────┘ ┌────┴────┐
│ │Yes │No
│ ▼ │
│ ┌─────────┐ │
│ │Gradual │ │
│ │adoption │ │
│ │or wait │ │
│ └─────────┘ │
│ │
└────────────┬────────────┘
▼
┌───────────────────────┐
│Try in small scope │
│and verify behavior │
└───────────────────────┘
The key point is "whether the code is pure and immutable." Since React Compiler optimizes based on the assumption of purity, codebases with many mutable operations or side effects may have more places where the compiler skips, or unexpected behavior may occur. According to the official documentation, changes in memoization may also affect useEffect firing timing. If immutable design is thoroughly implemented, adoption tends to go smoothly.
Having E2E tests makes it easier to verify behavior after adoption. Even without tests, adoption is possible, but it might be better to proceed carefully by pinning the compiler version (--save-exact). According to the official documentation, memoization behavior may change even in minor or patch versions, so manually verifying updates rather than auto-upgrading is recommended.
Current Thoughts
React Compiler 1.0 is a stable release with track record in Meta's production environment. Being freed from manual memoization is a significant benefit. It's genuinely nice that bugs from dependency array mistakes and time spent deciding how much to memoize will decrease.
If you're considering adoption, I think it would be good to first enable the ESLint plugin and check how many Rules of React violations exist in your codebase. The recommended or recommended-latest presets in eslint-plugin-react-hooks include compiler-specific rules and can be used even without installing the compiler. The unsupported-syntax rule can detect unsupported syntax, though I haven't been able to confirm whether it detects finally clauses or conditional branches inside try-catch. If you want to know for certain, you might want to try it in the React Compiler Playground or use tools like react-compiler-marker.
Note that there used to be a separate package called eslint-plugin-react-compiler, but it has now been integrated into eslint-plugin-react-hooks. If you have it installed, it's recommended to remove it and migrate to eslint-plugin-react-hooks@latest.
Top comments (0)