React Compiler Memoization Boundaries
React Compiler auto-memoizes most code—but effects, refs, and external stores still need manual attention.
React Compiler (formerly React Forget) promises to eliminate manual useMemo, useCallback, and React.memo. It analyzes your code at build time and automatically inserts memoization where beneficial. But it's not magic—there are boundaries where the compiler can't help.
How the Compiler Decides
React Compiler works by analyzing purity. If a value's computation is pure—same inputs always produce same outputs, no side effects—the compiler can safely cache it.
// ✅ Compiler handles this automatically
function ProductCard({ price, quantity }) {
const total = price * quantity; // Pure computation → auto-memoized
const formatted = formatCurrency(total); // Pure → auto-memoized
return <div>{formatted}</div>;
}The compiler tracks dependencies and only recomputes when inputs change—exactly what you'd do manually with useMemo.
Where the Compiler Can't Help
1. Effects and Their Dependencies
Effects are inherently impure—they're designed to have side effects. The compiler doesn't touch them:
function DataFetcher({ userId }) {
const [data, setData] = useState(null);
// ❌ Compiler doesn't optimize effects
useEffect(() => {
fetchUser(userId).then(setData);
}, [userId]); // Still need manual dep array
return <div>{data?.name}</div>;
}Effect cleanup, dependency arrays, and execution timing remain your responsibility.
2. Refs and Mutable Objects
Refs bypass React's reactivity—the compiler can't track mutations:
function Canvas({ onDraw }) {
const canvasRef = useRef(null);
const contextRef = useRef(null);
// ❌ Ref mutations are invisible to the compiler
useEffect(() => {
contextRef.current = canvasRef.current.getContext('2d');
}, []);
// ❌ Callback using ref—can't be auto-memoized safely
const draw = () => {
contextRef.current?.beginPath(); // Mutable ref access
onDraw(contextRef.current);
};
return <canvas ref={canvasRef} onClick={draw} />;
}Any function that reads or writes to refs is excluded from automatic memoization.
3. External Stores and Global State
State outside React's control (Redux, Zustand, global variables) can't be tracked:
import { useStore } from './store';
function Counter() {
const count = useStore(state => state.count);
// ❌ External store selector—compiler sees it as impure
const doubled = count * 2; // Might not be memoized!
return <div>{doubled}</div>;
}The compiler doesn't know when external stores change, so it's conservative about memoizing derived values.
4. Functions Passed from Non-Compiled Code
If a parent component isn't compiled, its callbacks aren't stable:
// Parent NOT using React Compiler
function LegacyParent() {
// This creates a new function every render
return <CompiledChild onClick={() => console.log('clicked')} />;
}
// Child IS compiled, but can't optimize around unstable prop
function CompiledChild({ onClick }) {
// onClick changes every render → downstream memoization breaks
const handler = () => {
onClick();
trackAnalytics();
};
return <button onClick={handler}>Click</button>;
}Gradual adoption creates boundaries where optimization stops.
Intentionally Opting Out
Sometimes you don't want memoization—like random values or timestamps:
function RandomGreeting() {
// This SHOULD be different every render
const greeting = greetings[Math.floor(Math.random() * greetings.length)];
return <h1>{greeting}</h1>;
}Use the "use no memo" directive to exclude specific functions:
function RandomGreeting() {
"use no memo"; // Opt out of compiler optimization
const greeting = greetings[Math.floor(Math.random() * greetings.length)];
return <h1>{greeting}</h1>;
}Or extract volatile logic to a separate function:
// Volatile computation isolated
function getRandomGreeting() {
"use no memo";
return greetings[Math.floor(Math.random() * greetings.length)];
}
// Component can be optimized normally
function RandomGreeting() {
const greeting = getRandomGreeting();
return <h1>{greeting}</h1>;
}Debugging Compiler Decisions
React DevTools shows what the compiler optimized:
- Components with a ✨ badge have compiler optimizations
- Hover to see which values are memoized
- Check "Highlight updates" to verify renders are skipped
The compiler also emits build-time warnings when it can't optimize:
// Build output shows what was skipped
⚠️ Cannot memoize: function reads mutable ref
⚠️ Cannot memoize: function has side effectsMigration Strategy
Keep Manual Memoization Where Needed
Don't remove all useMemo/useCallback immediately:
function DataGrid({ rows, columns, onCellClick }) {
// Keep this if onCellClick comes from non-compiled parent
const stableHandler = useCallback(
(cell) => onCellClick(cell),
[onCellClick]
);
// Compiler handles the rest
const processedRows = rows.map(transform);
return <Grid data={processedRows} onClick={stableHandler} />;
}Audit External Dependencies
Check if your state management library plays well with the compiler. Most modern libraries (Zustand, Jotai) work fine; older patterns might need adjustment.
The Takeaway
React Compiler handles ~80% of memoization automatically. The remaining 20%—effects, refs, external stores, gradual adoption boundaries—still needs manual attention. Don't assume "compiler handles everything" and remove all performance work. Audit the boundaries, use DevTools to verify, and keep manual optimization where the compiler explicitly can't help.
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Related Insights
Explore related edge cases and patterns
Advertisement