React Compiler: Stop Manually Optimizing Your React Apps
Source: Dev.to
During our team KATA session, a colleague asked a question that I bet you’ve thought about too:
“If React already knows to only render the elements that changed, why do we need to optimize anything manually?”
It was a brilliant question. The answer reveals a major pain point we’ve lived with for years—and shows how the React compiler addresses a few key areas.
Below is a journey through the evolution of React optimization, using a simple analogy: The Restaurant Kitchen.
🍝 The Restaurant Kitchen: How React Actually Works
Imagine your app is a kitchen.
| Role | Analogy |
|---|---|
| Head Chef (Parent Component) | Manages the whole kitchen. |
| Line Cooks (Child Components) | Handle specific stations. |
In a standard React app, every time the Head Chef changes something—even just restocking the salt—they ring a giant bell. Every single cook stops and re‑does their work, even if their station didn’t change.
React’s default behavior: When a parent re‑renders, all children re‑render.
For years we had to write extra code (hooks) to give instructions to React’s optimization engine. Let’s see how a single component evolved:
- Without hooks (no instructions to the compiler)
- With hooks (instructions to React’s optimization technique)
- React compiler code (optimizes automatically)
The Evolution of a Component
We’ll use a RestaurantMenu component that:
- Holds a list of dishes.
- Filters them (an expensive calculation).
- Renders a list of items (child components).
Phase 1: The Code (Clean but Slow)
This is the code most beginners write. It looks tidy, but it hides performance traps.
import { useState } from 'react';
// A simple child component
const DishList = ({ dishes, onOrder }) => {
console.log('🍝 Rendering DishList (Child)'); // {/* items... */};
};
export default function RestaurantMenu({ allDishes, theme }) {
const [category, setCategory] = useState('pasta');
// ⚠️ PROBLEM 1: Expensive calculation runs on every render
const filteredDishes = allDishes.filter(dish => {
console.log('🧮 Filtering... (Slow Math)');
return dish.category === category;
});
const handleOrder = dish => {
console.log('Ordered:', dish);
};
return (
<>
{/* Clicking this causes a re‑render */}
<button onClick={() => setCategory('salad')}>Switch Category</button>
{/* ⚠️ PROBLEM 2: Inline arrow function */}
{/* (dish) => handleOrder(dish) creates a brand‑new function on each render,
forcing DishList to re‑render. */}
<DishList dishes={filteredDishes} onOrder={dish => handleOrder(dish)} />
</>
);
}
What happens in the console?
Even a minor parent re‑render (e.g., clicking the button) triggers:
🧮 Filtering... (Slow Math)
🍝 Rendering DishList (Child)
Every interaction logs both statements—wasteful!
Phase 2: The Solution with Hooks (Additional Instructions)
To fix this we traditionally introduced hooks: useMemo, useCallback, and memo.
import { useState, useMemo, useCallback, memo } from 'react';
// Solution A: Wrap child in memo to prevent useless re‑renders
const DishList = memo(({ dishes, onOrder }) => {
console.log('🍝 Rendering DishList (Child)');
return /* items... */;
});
export default function RestaurantMenu({ allDishes, theme }) {
const [category, setCategory] = useState('pasta');
// Solution B: Cache calculation with useMemo
const filteredDishes = useMemo(() => {
console.log('🧮 Filtering... (Slow Math)');
return allDishes.filter(dish => dish.category === category);
}, [allDishes, category]);
// Solution C: Freeze function with useCallback
const handleOrder = useCallback(dish => {
console.log('Ordered:', dish);
}, []);
return (
<>
<button onClick={() => setCategory('salad')}>Switch Category</button>
{/* ⚠️ THE TRAP: We CANNOT use an inline arrow here!
If we wrote: onOrder={(dish) => handleOrder(dish)}
it would break the optimization because the wrapper creates a new reference.
We must pass the stable function directly. */}
<DishList dishes={filteredDishes} onOrder={handleOrder} />
</>
);
}
What happens in the console now?
If the parent re‑renders for a reason that doesn’t affect filteredDishes or handleOrder (e.g., theme changes), nothing logs:
(Silent. No logs appear.)
Performance is achieved, but the code becomes harder to read because of the extra hook boilerplate.
Note: If a teammate changes
onOrder={handleOrder}toonOrder={() => handleOrder()}, the optimization silently breaks—the arrow creates a new function each render.
Phase 3: The React Compiler Solution (No Extra Code)
Enter the React compiler (e.g., React 18’s automatic memoization). It can infer the same optimizations without explicit hooks.
import { useState } from 'react';
// No useMemo, no useCallback, no memo.
export default function RestaurantMenu({ allDishes, theme }) {
const [category, setCategory] = useState('pasta');
// The compiler automatically memoizes this expensive calculation.
const filteredDishes = allDishes.filter(dish => {
console.log('🧮 Filtering... (Slow Math)');
return dish.category === category;
});
// The compiler automatically stabilizes this function reference.
const handleOrder = dish => {
console.log('Ordered:', dish);
};
return (
<>
<button onClick={() => setCategory('salad')}>Switch Category</button>
{/* The child component can stay a plain function component. */}
<DishList dishes={filteredDishes} onOrder={handleOrder} />
</>
);
}
// Plain child component – no need for React.memo.
const DishList = ({ dishes, onOrder }) => {
console.log('🍝 Rendering DishList (Child)');
return /* items... */;
};
What happens now?
- The filter runs only when
allDishesorcategoryactually change. - The
handleOrderfunction retains a stable reference across renders. DishListre‑renders only when its props truly change.
All of this is achieved without the manual useMemo, useCallback, or memo boilerplate.
TL;DR
| Phase | What you write | What you get |
|---|---|---|
| 1 – Plain code | Simple, readable code | Unnecessary re‑renders & expensive work |
| 2 – Hook‑heavy | useMemo, useCallback, memo | Optimized but noisy & error‑prone |
| 3 – Compiler | Plain code again | Same (or better) performance automatically |
The React compiler lets you keep the clarity of Phase 1 while delivering the performance of Phase 2. No more “magic” inline‑arrow bugs, no more manual memoization—just write the way you think about UI and let the compiler do the heavy lifting.
React Compiler Magic – A Restaurant Analogy
const handleOrder = (dish) => {
console.log("Ordered:", dish);
};
return (
<>
<button onClick={() => setCategory('salad')}>Switch Category</button>
{/* ✅ COMPILER MAGIC: We can use an inline arrow again!
The compiler is smart enough to "memoize" this arrow function
wrapper automatically. It sees that `handleOrder` is stable,
so it makes this arrow stable too. */}
<DishList dishes={filteredDishes} onOrder={dish => handleOrder(dish)} />
</>
);
What happens in the console?
Even though we deleted all the hooks, the result is identical to Phase 2.
🖥️ CONSOLE OUTPUT:
---------------------------------------------
(Silent. No logs appear.)
What just happened?
The React Compiler analyzed your code at build time. It understands data flow better than we do.
- It sees
filteredDishesonly changes whencategorychanges. - It sees you wrapped
handleOrderin an arrow function(dish) => handleOrder(dish). - It automatically caches that arrow‑function wrapper so it remains the exact same reference across renders.
- It effectively generates the optimized code from Phase 2 for you, behind the scenes.
The Philosophy Shift
For years we had to manually tell the framework: “Remember this variable! Freeze this function!”
React Compiler addresses this problem!
React now assumes the burden of optimization. It lets us stop worrying about render cycles and dependency arrays and focus on what actually matters: shipping features.
What Now?
The best part is that React Compiler is backward compatible (React v17, v18 as well). You don’t have to rewrite your codebase. Just enable it, and it will optimize your “plain” components while leaving your existing hooks untouched.
Thanks for reading! This is my first post on Dev.to, and I wrote it to help solidify my own understanding of the Compiler. I’d love your feedback—did the restaurant analogy make sense to you? Let me know in the comments!