Deleting 400 Lines of useMemo with React Compiler 1.0 Cut TBT by 15%
What you need to know before handing memoization over to the compiler
Honestly, when I first learned useMemo, I overused it myself. Thinking "shouldn't everything expensive be wrapped?", I plastered useMemo and useCallback all over every component, until one day I found half the code filled with optimization hints. The ironic situation where memoization code outnumbered the actual feature code. But recently, in a real Next.js 16 project, I deleted 400 lines of useMemo-related code — and TBT (Total Blocking Time) actually dropped by 15%. I'll explain how that was possible, and how far you can safely delete.
What made this possible is React Compiler 1.0, officially released in October 2025. Because the compiler statically analyzes code at build time and handles memoization automatically, hundreds of lines of manually stacked optimization code are no longer needed. That said, it doesn't mean you can unconditionally delete every useMemo. This article is aimed at frontend developers who use React in production, and examines — with real code — how the compiler actually works and what patterns still require explicit useMemo.
Core Concepts
How the Compiler Replaces "Manual Hints"
The traditional useMemo and useCallback were manual hints that developers passed to the runtime saying "don't recalculate this value." Developers had to judge for themselves which values were expensive and which function references needed to be stable. In practice, this judgment is more complex than it sounds.
React Compiler reverses this process. It statically analyzes a component's data flow at build time, figures out which values change under which conditions on its own, and outputs JavaScript with automatic memoization code included.
// Before: developer manually judges and writes this by hand
function ProductCard({ price, discount }) {
const finalPrice = useMemo(
() => calculatePrice(price, discount),
[price, discount]
);
const handleBuy = useCallback(() => {
addToCart(finalPrice);
}, [finalPrice]);
return <button onClick={handleBuy}>{finalPrice} USD</button>;
}// After: the compiler handles it — this is all you need to write
function ProductCard({ price, discount }) {
const finalPrice = calculatePrice(price, discount);
const handleBuy = () => addToCart(finalPrice);
return <button onClick={handleBuy}>{finalPrice} USD</button>;
}The visible code becomes this clean, but the actual JavaScript output generated by the compiler is different. Rather than just handing things off to a black box, the compiler inserts code that directly tracks input changes.
// Output generated by React Compiler (conceptual representation)
import { c as _c } from "react/compiler-runtime";
function ProductCard({ price, discount }) {
const $ = _c(3); // allocate 3 cache slots
let finalPrice;
if ($[0] !== price || $[1] !== discount) {
finalPrice = calculatePrice(price, discount);
$[0] = price;
$[1] = discount;
$[2] = finalPrice;
} else {
finalPrice = $[2]; // return from cache if inputs are the same
}
const handleBuy = () => addToCart(finalPrice);
return <button onClick={handleBuy}>{finalPrice} USD</button>;
}Because it can manage the cache at a more granular input level than useMemo, in some cases it can be more precise than manual optimization.
Deopt (optimization bailout): When the compiler cannot guarantee safety through static analysis, it abandons automatic memoization for that component. This can occur with dynamic key access or irregular conditional branching, and can be visually confirmed in React DevTools.
Where useMemo Structurally Could Not Be Placed
Due to the Rules of Hooks, you cannot call hooks inside conditionals or after an early return. I remember situations where I had no choice but to give up optimization or forcibly split logic because of this constraint.
function DataTable({ data, isLoading }) {
if (isLoading) return <Spinner />;
// useMemo could not be used here (violates Rules of Hooks)
// so the expensive operation just runs on every render
const processedData = expensiveTransform(data);
return <Table rows={processedData} />;
}React Compiler has no such constraint. Even in a position after an early return, it can apply memoization by analyzing the data flow at compile time. This felt like the most practically significant improvement over the structural limitations of the old approach.
Practical Application
Example 1: Deleting 400 Lines of useMemo Code from a Next.js Dashboard
Here's a case from the Next.js 16 project I worked on. It started with adding a single line, reactCompiler: true, to next.config.ts.
// next.config.ts
const nextConfig = {
reactCompiler: true, // that's all it takes
};
export default nextConfig;The first thing I touched was the analytics dashboard component. useMemo and useCallback were attached to every prop, state, computation, and event handler, and more than half the component was optimization boilerplate.
// Before: analytics dashboard plastered with useMemo/useCallback
function AnalyticsDashboard({ userId, dateRange, rawData }) {
const [filters, setFilters] = useState(defaultFilters);
const filteredData = useMemo(
() => filterData(rawData, filters),
[rawData, filters]
);
const chartConfig = useMemo(
() => buildChartConfig(filteredData, dateRange),
[filteredData, dateRange]
);
const handleFilterChange = useCallback(
(key, value) => setFilters(prev => ({ ...prev, [key]: value })),
[]
);
const handleExport = useCallback(
() => exportData(filteredData, userId),
[filteredData, userId]
);
return (
<DashboardLayout>
<FilterPanel onChange={handleFilterChange} />
<Chart config={chartConfig} />
<ExportButton onClick={handleExport} />
</DashboardLayout>
);
}After enabling the compiler, I ran the existing tests first to confirm they passed, then removed the useMemo and useCallback calls.
// After: what it looks like once you hand things over to the compiler
function AnalyticsDashboard({ userId, dateRange, rawData }) {
const [filters, setFilters] = useState(defaultFilters);
const filteredData = filterData(rawData, filters);
const chartConfig = buildChartConfig(filteredData, dateRange);
const handleFilterChange = (key, value) =>
setFilters(prev => ({ ...prev, [key]: value }));
const handleExport = () => exportData(filteredData, userId);
return (
<DashboardLayout>
<FilterPanel onChange={handleFilterChange} />
<Chart config={chartConfig} />
<ExportButton onClick={handleExport} />
</DashboardLayout>
);
}Just looking at the code, you might wonder "is this really optimized?" Because the compiler inserts cache logic into the build output as shown above, the runtime behavior is identical to — or more finely optimized than — when useMemo was present. Here are the actual measurement results. TBT was measured in Chrome DevTools with low-spec mobile CPU throttling (6x slowdown) applied.
| Item | Before | After |
|---|---|---|
| Lines of code | ~40 lines | ~20 lines |
| Optimization approach | Manual useMemo/useCallback | Automatic compiler handling |
| TBT (mobile throttling baseline) | Baseline | 15% reduction |
| Code readability | More hooks than logic | Only business logic remains |
This is the result of the compiler preventing unnecessary re-renders more precisely than manual optimization. The perceptible difference is smaller on desktop.
Example 2: External Libraries and useEffect — Two Patterns That Still Need to Stay
After testing bulk removal of useMemo across three projects ranging from 10k to 150k LoC, most cases were fine. However, the two patterns below are ones I had to restore after experiencing mistakes firsthand.
Pattern 1: When an external library requires referential equality
function MapComponent({ lat, lng }) {
const mapConfig = useMemo(
() => ({ center: [lat, lng], zoom: 13 }),
[lat, lng]
);
useExternalMapLibrary(mapConfig);
return <div id="map" />;
}After removing useMemo, the problem of the map re-initializing on every render was found in staging. This is because the external map library interprets a change in the config object's reference as "new configuration has arrived." Since the compiler has no way to know the internal behavior of external libraries, it's recommended to explicitly keep this in place.
Referential Equality: In JavaScript, objects and functions have different references each time they are created, even if their content is the same.
{ a: 1 } === { a: 1 }isfalse. Some external libraries oruseEffectrecognize a changed reference as "the value has changed."
Pattern 2: When preventing over-execution in a useEffect dependency array
function ObserverComponent({ threshold }) {
const targetRef = useRef(null);
const config = useMemo(() => ({ threshold }), [threshold]);
useEffect(() => {
const observer = new IntersectionObserver(callback, config);
return () => observer.disconnect();
}, [config]);
return <div ref={targetRef} />;
}Removing useMemo from config causes the inline object to create a new reference on every render, making useEffect unnecessarily re-execute continuously even when the threshold value hasn't changed. In this case, the developer must manually ensure referential stability of the useEffect dependency.
Pros and Cons Analysis
Advantages
| Item | Details |
|---|---|
| Code simplification | Optimization hint code is removed, leaving only business logic |
| Extended coverage | Optimization applied even after early returns and other positions where useMemo couldn't be placed |
| Team consistency | Even if a junior developer misses a useMemo, the compiler covers it |
| Enhanced ESLint | Compiler-based rules integrated into eslint-plugin-react-hooks v5+, no separate installation needed |
| Ecosystem integration | Available with a single-line configuration in Next.js 16, Expo SDK 54, and Vite |
Disadvantages and Caveats
| Item | Details | Mitigation |
|---|---|---|
| Bulk deletion risk | Deleting useMemo all at once can cause useEffect dependency issues | Enable compiler first, then remove incrementally |
| Deopt cases | Optimization abandoned for some patterns such as dynamic keys or complex conditional branching | Check for deopt in React DevTools, then handle manually |
| Double memoization | Existing useMemo + compiler auto-handling may be applied redundantly | Write new code without useMemo, clean up old code sequentially |
| RSC limitations | Compiler behavior is limited in React Server Components environments | Apply primarily to client components |
RSC (React Server Components): Components that only run on the server and are not included in the client bundle. Since memoization is a concept tied to client-side re-rendering, the compiler's benefits are limited in RSC.
The Most Common Mistakes in Practice
- Bulk-deleting useMemo the moment you enable the compiler — The compiler and existing code can coexist. The safe flow is to pass tests first, then remove incrementally in a separate PR.
- Moving on without checking deopted components — In React DevTools, you can visually see which components the compiler optimized and where it gave up. Skipping this step means missing unintended performance regressions.
- Also removing useMemo from external library reference code — The compiler cannot know the internal behavior of external libraries. When referential stability is required by an external contract, explicit useMemo is still necessary.
Closing Thoughts
React Compiler is not saying "don't use useMemo" — it's a paradigm shift that hands over to the compiler the cognitive cost developers were spending on "placement judgment" for memoization. The judgment itself doesn't disappear; it means a tool that makes that judgment better has arrived.
Here are 3 steps you can start right now.
- Enable the compiler first. For Next.js 16, add
reactCompiler: truetonext.config.ts; for Vite, installvite-plugin-react-compiler. You don't need to touch existinguseMemoyet. It can coexist with existing code. - Check for deopted components with React DevTools. You can visually see which components the compiler optimized and where it gave up. If there are any missed components, identifying the cause is the next step.
- Write new code without useMemo from here on. It's the best way to build intuition for trusting the compiler. For cleaning up existing code, it's recommended to split it into a separate PR after tests are stably passing, and proceed incrementally.
References
- React Compiler v1.0 Official Blog | react.dev
- useMemo Official Docs | react.dev
- React Compiler Introduction | react.dev
- Stop Writing useMemo and useCallback: Migration Guide to React Compiler 1.0 | adeelhere.com
- Goodbye useMemo: How the React Compiler Changed My Next.js Codebase Forever | Medium
- The React Compiler Is Here: You Can Delete Half Your useMemo and useCallback | DEV Community
- React Compiler vs. useMemo: Real Benchmarks | DEV Community
- Codemod: react/19/remove-memoization | codemod.com
- I tried React Compiler today | Developer Way
- React Compiler: An Introduction, Pros, Cons & When to Use It | DebugBear