The React Compiler - Delete Your useMemo and useCallback. Seriously.
How React Compiler v1.0 makes manual memoization obsolete, with real-world before/after examples from production codebases

I need to get something off my chest.
I have mass-imported useMemo and useCallback into hundreds of files across my career. I have sat in code reviews debating whether a computation is "expensive enough" to warrant memoization. I have stared at dependency arrays like they're sudoku puzzles, triple-checking that I didn't miss a reference. I have written React.memo() wrappers around components that probably didn't need them, just because the profiler showed an extra re-render and someone on the team panicked.
All of that work, all of it was doing the compiler's job for it. Badly.
React Compiler v1.0 shipped stable in October 2025. It's been running in production at Meta (Quest Store), Sanity Studio (1,231 components compiled), and Wakelet (100% of users). The results aren't subtle. And the implication is clear: manual memoization in React was always a design compromise, and the compiler is the real fix.
Let me walk you through exactly what's changed, with real code from real apps.
The Dirty Truth About Manual Memoization
Here's something nobody talks about in tutorials: most useMemo and useCallback calls in production codebases are either wrong, unnecessary, or actively harmful.
I'm not exaggerating. Let me show you the patterns I see constantly:
Pattern 1: Memoizing Cheap Work
// I see this EVERYWHERE. And it makes me sad.
function UserGreeting({ user }) {
const fullName = useMemo(
() => `\({user.firstName} \){user.lastName}`,
[user.firstName, user.lastName]
);
return <h1>Hello, {fullName}</h1>;
}
String concatenation. You memoized string concatenation. The overhead of checking the dependency array, storing the cached value, and comparing references on every render is almost certainly more expensive than just computing the string. But someone read "use useMemo for derived values" in a blog post and now it's in 47 components.
Pattern 2: The useCallback That Doesn't Actually Help
function TodoList({ todos }) {
// This useCallback is completely useless
const handleDelete = useCallback((id) => {
deleteTodo(id);
}, []);
return todos.map(todo => (
// ...but this inline arrow creates a new function every render anyway
<TodoItem key={todo.id} onDelete={() => handleDelete(todo.id)} />
));
}
You wrapped handleDelete in useCallback for "performance." Feels responsible, right? But the () => handleDelete(todo.id) arrow function in the JSX creates a brand new function reference on every render regardless. The useCallback did nothing. And even if TodoItem is wrapped in React.memo, it'll still re-render because its onDelete prop is a new reference every time.
This is the exact kind of pattern the React Compiler handles correctly. Let me show you.
Pattern 3: The Memo Sandwich
// The defensive memoization combo that makes components unreadable
const ExpensiveList = memo(function ExpensiveList({ items, onSelect, filter }) {
const filtered = useMemo(
() => items.filter(item => item.category === filter).sort((a, b) => a.name.localeCompare(b.name)),
[items, filter]
);
const handleSelect = useCallback(
(id) => { onSelect(id); },
[onSelect]
);
return (
<ul>
{filtered.map(item => (
<ListItem key={item.id} item={item} onSelect={handleSelect} />
))}
</ul>
);
});
const ListItem = memo(function ListItem({ item, onSelect }) {
return <li onClick={() => onSelect(item.id)}>{item.name}</li>;
});
Count the memoization primitives: memo × 2, useMemo × 1, useCallback × 1. Four manual optimization hints for what is fundamentally just "render a filtered list." And even with all this ceremony, the () => onSelect(item.id) in ListItem still breaks memoization because it's a new function reference every render.
This is the kind of code that makes senior engineers feel productive and junior engineers feel confused. It's also the exact kind of code that the React Compiler turns into a clean, automatically-optimized version.
What the Compiler Actually Does
The React Compiler is a build-time Babel plugin (with SWC support coming) that analyzes your component's data flow and automatically inserts memoization where it actually helps. It doesn't use React.memo, useMemo, or useCallback under the hood, it generates its own optimized caching at a more granular level.
Here's the key insight: the compiler can memoize conditional code paths that manual hooks literally cannot express.
// The compiler can optimize THIS correctly
function Dashboard({ user, theme }) {
if (user.role === 'admin') {
return <AdminPanel user={user} theme={theme} />;
}
return <UserPanel user={user} />;
}
Try wrapping the admin-branch computation in useMemo. You can't, hooks can't be called conditionally. The compiler doesn't have this limitation because it operates at the AST level, not the runtime level. It sees both branches and optimizes each independently.
The Memo Sandwich - After the Compiler
Remember that defensive memoization monstrosity from earlier? Here's what the exact same component looks like when you let the compiler do its job:
// After: Write normal code. The compiler handles the rest.
function ExpensiveList({ items, onSelect, filter }) {
const filtered = items
.filter(item => item.category === filter)
.sort((a, b) => a.name.localeCompare(b.name));
return (
<ul>
{filtered.map(item => (
<ListItem key={item.id} item={item} onSelect={onSelect} />
))}
</ul>
);
}
function ListItem({ item, onSelect }) {
return <li onClick={() => onSelect(item.id)}>{item.name}</li>;
}
No memo. No useMemo. No useCallback. Just plain React components doing their thing. The compiler analyzes the data flow, determines that filtered only needs recomputing when items or filter change, stabilizes the function references passed to children, and skips re-rendering ListItem when its props haven't meaningfully changed.
And here's the kicker: the compiler handles the () => onSelect(item.id) arrow function correctly, something that was impossible with manual memoization without restructuring the component.
Real-World Proof: The Numbers Don't Lie
Let's talk production results. Not benchmarks. Not synthetic tests. Actual apps serving real users.
Meta Quest Store
12% faster initial page loads
Over 2.5× faster interactions
Memory usage: neutral (no increase despite added caching)
Sanity Studio
1,231 out of 1,411 components compiled successfully
20–30% reduction in render time and latency
Real-time collaborative editing (no save button, multiplayer, constant state reconciliation) became noticeably smoother
180 remaining components need refactoring to comply with the Rules of React. The compiler helped surface those violations
Wakelet
10% LCP improvement (2.6s → 2.4s)
15% INP improvement (275ms → 240ms)
~30% INP improvement on pure React components specifically
Rolled out to 100% of users in November 2025
No major regressions reported
Bonus: the compiler surfaced existing bugs that excessive re-rendering had been hiding
That last point from Wakelet is my favorite. The compiler didn't just make things faster, it exposed data races and state bugs that were previously invisible because components were re-rendering so often they accidentally masked timing issues. Your sloppy code was hiding bugs, and the compiler's optimization revealed them.
A Real Refactor: E-commerce Product Page
Let me walk through a realistic component to show what a compiler-ready refactor looks like. This isn't a contrived example, it's the kind of code you'd find in any e-commerce frontend.
Before: The Memoization Tax
import { useState, useMemo, useCallback, memo } from 'react';
const ProductPage = memo(function ProductPage({ product, reviews, onAddToCart }) {
const [selectedVariant, setSelectedVariant] = useState(product.variants[0].id);
const currentVariant = useMemo(
() => product.variants.find(v => v.id === selectedVariant),
[product.variants, selectedVariant]
);
const averageRating = useMemo(
() => reviews.length > 0
? reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length
: 0,
[reviews]
);
const sortedReviews = useMemo(
() => [...reviews].sort((a, b) => new Date(b.date) - new Date(a.date)),
[reviews]
);
const discountedPrice = useMemo(
() => currentVariant
? currentVariant.price * (1 - (product.discount || 0))
: 0,
[currentVariant, product.discount]
);
const handleAddToCart = useCallback(() => {
onAddToCart({
productId: product.id,
variantId: selectedVariant,
price: discountedPrice,
});
}, [onAddToCart, product.id, selectedVariant, discountedPrice]);
const handleVariantChange = useCallback((variantId) => {
setSelectedVariant(variantId);
}, []);
return (
<div>
<h1>{product.name}</h1>
<PriceDisplay price={discountedPrice} original={currentVariant?.price} />
<StarRating rating={averageRating} count={reviews.length} />
<VariantPicker
variants={product.variants}
selected={selectedVariant}
onChange={handleVariantChange}
/>
<button onClick={handleAddToCart}>Add to Cart</button>
<ReviewList reviews={sortedReviews} />
</div>
);
});
Count them: 1 memo, 4 useMemo, 2 useCallback. Seven manual optimization hints. And I guarantee the dependency arrays have been a source of bugs at least once. (discountedPrice depends on currentVariant which depends on selectedVariant, miss one and you've got stale data.)
After: Compiler-Friendly Code
import { useState } from 'react';
function ProductPage({ product, reviews, onAddToCart }) {
const [selectedVariant, setSelectedVariant] = useState(product.variants[0].id);
const currentVariant = product.variants.find(v => v.id === selectedVariant);
const averageRating = reviews.length > 0
? reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length
: 0;
const sortedReviews = [...reviews].sort((a, b) => new Date(b.date) - new Date(a.date));
const discountedPrice = currentVariant
? currentVariant.price * (1 - (product.discount || 0))
: 0;
return (
<div>
<h1>{product.name}</h1>
<PriceDisplay price={discountedPrice} original={currentVariant?.price} />
<StarRating rating={averageRating} count={reviews.length} />
<VariantPicker
variants={product.variants}
selected={selectedVariant}
onChange={setSelectedVariant}
/>
<button onClick={() => onAddToCart({
productId: product.id,
variantId: selectedVariant,
price: discountedPrice,
})}>
Add to Cart
</button>
<ReviewList reviews={sortedReviews} />
</div>
);
}
Zero memoization hooks. Seven fewer imports. The component reads like a straightforward description of what it renders. The compiler sees every computation, traces the data flow, and inserts exactly the right caching, including stabilizing the inline onClick handler and the setSelectedVariant reference passed to VariantPicker.
This isn't wishful thinking. This is how the compiler works today, in stable v1.0, on React 17+.
When You Should Still Keep Manual Memoization
I'm not going to pretend the compiler replaces 100% of manual memoization use cases. Here are the situations where you should keep useMemo/useCallback:
1. Effect Dependencies
If a memoized value is used as a useEffect dependency, you might want explicit control over its reference stability. The compiler's memoization is usually sufficient, but useMemo gives you a guarantee.
// Keep this — you need explicit control over when the effect fires
const config = useMemo(() => ({ endpoint, timeout }), [endpoint, timeout]);
useEffect(() => {
const client = createClient(config);
return () => client.disconnect();
}, [config]);
2. Third-Party Library Interop
Some libraries compare prop references internally. If they aren't compiler-aware, you might need to manually stabilize values:
// Keep this if the charting library does shallow comparison internally
const chartData = useMemo(() => transformData(rawData), [rawData]);
<ThirdPartyChart data={chartData} />
Material UI's useTheme(), TanStack Query's useMutation(), and React Router's useLocation() are known to return new objects on every render, which can break memoization chains even with the compiler.
3. Genuinely Expensive Computations
If you're doing something that takes >1ms (and you've measured it, not guessed), explicit memoization documents the intent:
// Keep this — the comment explains WHY, not just WHAT
const clusteredPoints = useMemo(
() => runDBSCAN(points, epsilon, minPoints), // ~15ms on 10k points
[points, epsilon, minPoints]
);
The rule of thumb: If you can't explain why the memoization matters in a code comment, the compiler should handle it.
Setting It Up (It's Embarrassingly Easy)
Install
npm install --save-dev --save-exact babel-plugin-react-compiler@latest
Vite
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: ['babel-plugin-react-compiler'],
},
}),
],
});
Next.js
// next.config.js
module.exports = {
experimental: {
reactCompiler: true,
},
};
Next.js 16 marks this as stable. For Next.js ≥15.3.1, it's fully supported.
Expo
Expo SDK 54+ enables the compiler by default. If you're starting a new Expo project, you're already using it.
Incremental Adoption
Don't want to enable it everywhere at once? Scope it to a directory:
// babel.config.js
module.exports = {
overrides: [
{
test: './src/features/**/*.{js,jsx,ts,tsx}',
plugins: ['babel-plugin-react-compiler'],
},
],
};
Verify It's Working
Open React DevTools. Compiled components show a ✨ Memo badge. If you see it, the compiler processed that component. (Note: the badge means "processed," not "guaranteed optimized", the compiler skips components it deems unsafe to optimize.)
The Migration Playbook
Here's how I'd approach migrating an existing codebase. I've done this twice now, and this order matters:
Step 1: Enable the ESLint plugin first. Install eslint-plugin-react-hooks@latest and use the recommended preset. The compiler-powered lint rules will flag components that violate the Rules of React, these are the ones the compiler can't optimize.
Step 2: Fix the violations. Common issues: mutating props, side effects during render, reading refs during render. These were always bugs; the compiler just makes them visible.
Step 3: Enable the compiler on a non-critical path. A settings page, an about page, something with low traffic where you can validate behavior.
Step 4: Profile, don't guess. Use React DevTools Profiler. Compare render counts and timings before/after. The new Performance Tracks in DevTools (React 19.2) make this much easier.
Step 5: Expand gradually. Enable for more directories. Run your test suite. Check for regressions. The Sanity Studio team reported that they didn't need a single 'use no memo' directive in their entire rollout, your mileage may vary, but the compiler is very conservative about what it optimizes.
Step 6: Stop writing new useMemo/useCallback. For new code, just write plain components and let the compiler work. Keep the existing memoization hooks in old code, removing them can change the compiler's output, so test before deleting.
The Bigger Picture
The React Compiler is the culmination of almost a decade of work. The React team's first exploration into compilers started with Prepack in 2017. That project was shut down, but it directly informed the design of Hooks, which were built with a future compiler in mind. Let that sink in. Hooks were always meant to be temporary manual optimization until the compiler could do it automatically.
useMemo and useCallback were the bicycle. The compiler is the car. You can still ride the bicycle if you want. But if you're commuting to work every day, you're making your life harder than it needs to be.
The React Foundation, backed by Amazon, Microsoft, Expo, Vercel, and others, is now overseeing React's development. An SWC plugin is in the works for faster build times. The compiler will continue to get smarter with each release.
The era of "memo everything and pray" is over. Write simple, pure components. Let the compiler do the rest.
Your code will be cleaner. Your bundles will be smaller. Your team will be happier. And you'll never have to debug a stale useMemo dependency array at 2 AM again.
Now go delete some hooks.
If this hit home, follow for more opinionated React takes. Got a horror story about useMemo? Drop it in the comments , I'm collecting them.




