DEV Community

Yuvaraj
Yuvaraj

Posted on

React Compiler: Stop Manually Optimizing Your React Apps

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:

  1. Holds a list of dishes.
  2. Filters them (an expensive calculation).
  3. 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

(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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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.)

Enter fullscreen mode Exit fullscreen mode

(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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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.)

Enter fullscreen mode Exit fullscreen mode

What just happened?
The React Compiler analyzed your code at build time. It understands data flow better than we do.

  • It sees filteredDishes only changes when category changes.
  • 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!

Top comments (0)