During our team KATA session, a colleague asked a question that I bet you’ve thought about it 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 let’s see how React compiler addresses few areas.
Let’s take 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.
- Head Chef (Parent Component): Manages the kitchen.
- Line cooks (Child Components): Handle specific stations.
In a standard React app, every time the Head Chef changes something—even just restocking the…
During our team KATA session, a colleague asked a question that I bet you’ve thought about it 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 let’s see how React compiler addresses few areas.
Let’s take 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.
- Head Chef (Parent Component): Manages the 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 redoes their work, even if their specific station didn’t change.
This is React’s default behavior: When a parent re-renders, all children re-render.
For years, to stop this waste, we had to write additional code to give instruction(hooks) to react’s optimisation technique. Let’s look at how a single component evolved from "without hooks(instructions to compiler)" to "With hooks(instructions to react optimisation technique)" to "React compiler code automatically optimises it."
The Evolution of a Component
Let’s look at a RestaurantMenu that does three things:
- Holds a list of dishes.
- Filters them (an expensive calculation).
- Renders a list of items (child components).
Phase 1: The Code (Clean but Slow)
Here is the code most beginners write. It looks clean, but it has hidden performance traps.
import { useState } from 'react';
// A simple child component
const DishList = ({ dishes, onOrder }) => {
console.log("🍝 Rendering DishList (Child)"); // <--- Watch this log!
return <div>{/* items... */}</div>;
};
export default function RestaurantMenu({ allDishes, theme }) {
const [category, setCategory] = useState('pasta');
// ⚠️ PROBLEM 1: Expensive Calculation runs every render
const filteredDishes = allDishes.filter(dish => {
console.log("🧮 Filtering... (Slow Math)");
return dish.category === category;
});
const handleOrder = (dish) => {
console.log("Ordered:", dish);
};
return (
<div className={theme}>
{/* Clicking this causes a re-render */}
<button onClick={() => setCategory('salad')}>Switch Category</button>
{/* ⚠️ PROBLEM 2: Inline Arrow Function */}
{/* Writing (dish) => handleOrder(dish) creates a BRAND NEW function
in memory every single time this component renders.
This forces DishList to re-render. */}
<DishList
dishes={filteredDishes}
onOrder={(dish) => handleOrder(dish)}
/>
</div>
);
}
What happens in the Console? Even if the parent re-renders for a minor reason (or if we click the button), everything runs again.
🖥️ CONSOLE OUTPUT:
---------------------------------------------
🧮 Filtering... (Slow Math)
🍝 Rendering DishList (Child)
(Every single interaction triggers these logs. Wasteful!)
Phase 2: The Solution with hooks(addition instructions)
To fix this in React, we had to introduce "Hooks." We wrap in 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 <div>{/* items... */}</div>;
});
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 (
<div className={theme}>
<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 arrow wrapper
is a new reference. We are FORCED to pass the function directly. */}
<DishList
dishes={filteredDishes}
onOrder={handleOrder}
/>
</div>
);
}
What happens in the Console now?
If the parent re-renders (for example, if theme changes but category stays the same), the console stays silent.
🖥️ CONSOLE OUTPUT:
---------------------------------------------
(Silent. No logs appear.)
(Performance is achieved, but the code is hard to read because of hooks syntax)
What happens, If your colleague changes onOrder={handleOrder} to onOrder={() => handleOrder()}, the optimization breaks silently, the arrow function () => handleOrder() creates a new function every time the component renders
Phase 3: The React Compiler Solution (without additional code)
This is the magic of React compiler. You go back to writing the code from Phase 1.
// No useMemo. No useCallback. No memo.
export default function RestaurantMenu({ allDishes, theme }) {
const [category, setCategory] = useState('pasta');
// The Compiler AUTOMATICALLY memoizes this
const filteredDishes = allDishes.filter(dish => {
console.log("🧮 Filtering... (Slow Math)");
return dish.category === category;
});
// The Compiler AUTOMATICALLY stabilizes this function
const handleOrder = (dish) => {
console.log("Ordered:", dish);
};
return (
<div className={theme}>
<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)} />
</div>
);
}
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 handleOrder in 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 address this problem!. React now assumes the burden of optimization. It allows us to stop worrying about render cycles and dependency arrays, and start focusing 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. You can enable it, and it will optimize your "plain" components while leaving your existing hooks.
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!