The Farrelly in text on fire

The React Compiler is out!

22nd of December, 2025
9 minute read

The React team have been busy at work creating an optimisation tool which can fit seamlessly right into your build process. Letting developers remain focused on landing features rather than meticulously adding useCallback and useMemo on every single function you use throughout your React app 😏 We'll have a look under the hood at what it actually does to optimise your code, along with how to add it to your project. All with examples lifted directly from the great React docs for the React Compiler.

So what does the React Compiler do?

It handles memoising your React code so you don't have to. It focuses on two specific areas of optimisation. Skipping cascading re-renders and skipping expensive calculations that are outside of React. Veteran React users will know that the majority of React optimisations really boil down to the both of those. Make sure you only render exactly as often as you need to and memoise/cache what you can. Now you get improved update peformance without having to exert any effort.

How does the React Compiler transform the code?

Lucky for us, the React team have provided a playground where you can see the result of your code being compiled down. This provides an approachable surface for us to understand how the compiler applies its magic.

Using the playground we can breakdown the following code samples in order to get to grips with both types of optimisation that are applied.

Reducing cascading re-renders

Classic React wisdom states, "if an element keeps updating, isolate it into a separate component in order to not cause re-render other unrelated elements". In this code sample we can see that any change to the 'friends' array will cause the entire JSX return have to be re-created. If we were to put the map into its own element we could avoid causing the other elements to have to re-render. However, with the compiler in our pipeline we don't need to because it'll do exactly that for us.

function FriendList({ friends }) {
  const onlineCount = useFriendOnlineCount();
  if (friends.length === 0) {
    return <NoFriends />;
  }
  return (
    <div>
      <span>{onlineCount} online</span>
      {friends.map((friend) => (
        <FriendListCard key={friend.id} friend={friend} />
      ))}
      <MessageButton />
    </div>
  );
}

Compiler output

import { c as _c } from "react/compiler-runtime";
function FriendList(t0) {
  const $ = _c(9);
  const { friends } = t0;
  const onlineCount = useFriendOnlineCount();
  if (friends.length === 0) {
    let t1;
    if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
      t1 = <NoFriends />;
      $[0] = t1;
    } else {
      t1 = $[0];
    }
    return t1;
  }
  let t1;
  if ($[1] !== onlineCount) {
    t1 = <span>{onlineCount} online</span>;
    $[1] = onlineCount;
    $[2] = t1;
  } else {
    t1 = $[2];
  }
  let t2;
  if ($[3] !== friends) {
    t2 = friends.map(_temp);
    $[3] = friends;
    $[4] = t2;
  } else {
    t2 = $[4];
  }
  let t3;
  if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
    t3 = <MessageButton />;
    $[5] = t3;
  } else {
    t3 = $[5];
  }
  let t4;
  if ($[6] !== t1 || $[7] !== t2) {
    t4 = (
      <div>
        {t1}
        {t2}
        {t3}
      </div>
    );
    $[6] = t1;
    $[7] = t2;
    $[8] = t4;
  } else {
    t4 = $[8];
  }
  return t4;
}
function _temp(friend) {
  return <FriendListCard key={friend.id} friend={friend} />;
}

Briefly, before we get back to talking about the optimisation applied here. Let's breakdown exactly what's happening. Let's divide and conquer by beginning with the first if statement

  if (friends.length === 0) {
    let t1;
    if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
      t1 = <NoFriends />;
      $[0] = t1;
    } else {
      t1 = $[0];
    }
    return t1;
  }

So given that friends.length is 0, we enter the statement. Where there's a comparison check for the 0th index of the $ array to be equal to "react.memo_cache_sentinel". The symbol's intent is to signify that the array element is empty. The array is constructed via this line

const $ = _c(9);

It returns nine elements all equal to this 'empty' symbol. So bringing it all together, if the 0th index is empty then create the <NoFriends /> element and set it at that index. If the index isn't equal to the symbol it must mean we've already fulfilled it so the else just returns the index.

This is the pattern we see throughout all of the React Compiler code. If the index is 'empty' or the value has changed then construct the React elements and set them in the index. Otherwise just return the elements already at the index. Let's move to the next section to see that occur again.

  let t1;
  if ($[1] !== onlineCount) {
    t1 = <span>{onlineCount} online</span>;
    $[1] = onlineCount;
    $[2] = t1;
  } else {
    t1 = $[2];
  }

So given what we have inside the 1st array index if it's not equal to the value of onlineCount we reconstruct the React element and return it. Otherwise, we just return the index and carry on.

Putting all of this code together we can see this pattern reoccurring for each branching of code. What this provides is our memoization, given the same prop values we get given the same result. It doesn't need to be reconstructed because it's within the memory of the $ array.

To tie this back in, the hidden optimisation that the React Compiler has given us as well is skipping the cascading re-renders caused by the map. Even though in classic React fashion the value being mapped on would cause a cascading re-render for all components present. It doesn't here. As it's also split into this optimisation if statement breakdown. If the value for friends change it will only affect its own elements, not others.

Expensive functions outside of React

The memoisation applied for functions is essentially the same. It's important to note that the optimisations the React Compiler applies is limited to React-scoped functions. As in, it affects only Components, Hooks, and functions used within them. A function from outside of the React scope isn't and can't really be optimised for React. Let me show you what I mean

// **Not** memoized by React Compiler, since this is not a component or hook
function expensivelyProcessAReallyLargeArrayOfObjects() { /* ... */ }

// Memoized by React Compiler since this is a component
function TableContainer({ items }) {
  // This function call would be memoized:
  const data = expensivelyProcessAReallyLargeArrayOfObjects(items);

  return (
    <div>
      {data.map(i => <Item item={i}/>)}
    </div>
  )
}

Provides the following output

import { c as _c } from "react/compiler-runtime"; // **Not** memoized by React Compiler, since this is not a component or hook
function expensivelyProcessAReallyLargeArrayOfObjects() {
  /* ... */
}

// Memoized by React Compiler since this is a component
function TableContainer(t0) {
  const $ = _c(4);
  const { items } = t0;
  let t1;
  if ($[0] !== items) {
    const data = expensivelyProcessAReallyLargeArrayOfObjects(items);
    t1 = data.map(_temp);
    $[0] = items;
    $[1] = t1;
  } else {
    t1 = $[1];
  }
  let t2;
  if ($[2] !== t1) {
    t2 = <div>{t1}</div>;
    $[2] = t1;
    $[3] = t2;
  } else {
    t2 = $[3];
  }
  return t2;
}
function _temp(i) {
  return <Item item={i} />;
}

If we focus in on where the expensive function is being called, we can see that it's got the same optimisation applied.

  if ($[0] !== items) {
    const data = expensivelyProcessAReallyLargeArrayOfObjects(items);
    t1 = data.map(_temp);
    $[0] = items;
    $[1] = t1;
  } else {
    t1 = $[1];
  }

If the value for items changes, call the expensive function and traverse through the map. Then set the values into the two available indices.

However the function used outside of React isn't optimised at all as we can see. The input and output are exactly the same.

How can I install it?

The React team have provided a babel plugin for purely React projects. Whilst frameworks like Vite, NextJS, React Router, Expo and bundlers such as Webpack, Rspack, Rsbuild have provided guidance in their own docs.

If you have a React, or React-Native project the installation process is as simple as adding the aforementioned babel plugin into your babel.config.js file as shown

module.exports = {
    plugins: [
        'babel-plugin-react-compiler', // must run first!
        // ... other plugins
    ],
    // ... other config
};

For a NextJS project it's as simple as setting the reactCompiler prop within your next.config after adding the babel config.

const nextConfig = {
    ...
    experimental: {
        reactCompiler: true,
    },
    ...
}

You can verify that your code has received the compiled optimisations by using the React Devtools. Browsing through the "Components" tab will show you the ✨ emoji for the optimised components.

Infact, if you have the dev tools already setup. You can inspect this article via the Components tab and see 'Memo ✨' for yourself.

TheFarrelly 2025

Made with ❤️

Free Palestine! 🇵🇸