React

An Intro to React Compiler

A glimpse at the future of React memoization

  • 11 min read
  • February 07, 2025
  • 282 views

Introduction

React Compiler is the new shining star on the React horizon, promising the end of useMemo, useCallback and React.memo – but how does it work? Let's explore what React Compiler is, how it works, and what it means for your code and the future of React development.

Memoization Recap

Before we can start diving into what React Compiler does, let's revisit memoization in React. Consider the following code:

function ExpensiveTree() {
    const now = performance.now();
    while (performance.now() - now < 1000) {}

    return <>Hello, world!</>;
}

function Counter() {
    const [count, setCount] = useState(1);

    return (
        <>
            <span>The current count is: {count}</span>
            <button onClick={() => setCount((prev) => prev++)}>
                Increment
            </button>
            <ExpensiveTree />
        </>
    );
}

We can see two React components; a basic counter, and a component called ExpensiveTree, the sole purpose of which is to reflect a slow part of the application. So what happens when we click the button to increment the counter? The counter does increase – but it does so with a noticeable delay of about one second.

Why does this happen?

When a React component's state updates, like our count variable does, the component re-renders. And by default, so do all of the components children, regardless of whether they actually consume the count variable in some way. Our <ExpensiveTree /> component from above has no idea the count state even exists, and yet, it will re-render. This is because React has no way to know for sure that no dependencies exist. State might be consumed through contexts, for example, so to be sure, React chooses to re-render everything that could potentially be affected.

To prevent this, we can opt for a technique called memoization. It allows us to tell React to only update if some given set of dependencies has actually changed. There are a couple of ways to memoize values: In our example above, the easiest way to memoize would be to wrap our ExpensiveTree component in a call to React.memo. By default, this function will compare the previous component props to the new ones by performing a shallow comparison, and only perform a re-render if any of the props have changed.

Other ways to memoize values in React are React.useMemo and React.useCallback. Instead of on the component level, these hooks can be used to memoize values and functions, respectively.

Let's look at an example where our <Counter> needs to perform some sort of expensive, external computation:

function Counter() {
    const [count, setCount] = useState(0);
    const computationResult = performExpensiveComputation(count);

    return (
        <>
            <button onClick={() => setCount((prev) => prev + 1)}>
                Increment
            </button>
            <p>Count: {count}</p>
            <p>Computation Result: {computationResult}</p>
        </>
    );
}

In this case, we choose to just go ahead and compute away. If the computation ends up taking a couple hundred of milliseconds, or even a couple of seconds, then our counter will stall any re-render occuring further up the component tree. To avoid this, we can wrap our computation with a call to React.useMemo, and specify the necessary dependencies:

function Counter() {
    const [count, setCount] = useState(0);

    const computationResult = useMemo(() => {
        return performExpensiveComputation(count);
    }, [count]);

    return (
        <>
            <button onClick={() => setCount((prev) => prev + 1)}>
                Increment
            </button>
            <p>Count: {count}</p>
            <p>Computation Result: {computationResult}</p>
        </>
    );
}

This way, the computation will only be performed when the count state changes. That is how memoization works in React, and if you're worked with React for more than a couple of days, I'm sure you will have run into similar code already. So what's the big deal?

Manual Memoization

Before I address the elephant in the room, let's step back for a moment and look at the bigger picture of memoization. I promise it will be worth it!

I'll present you with a challenge for now: Imagine that all of React's memoization utilities were to suddenly disappear. No React.memo, and no useMemo either. You're on your own! What would you do? Can you somehow memoize the code from our example above, without resorting to React's internal magic?

I think we can!

To make this work, we need to satisfy the following:

  • We need to somehow store and be able to refer to the dependencies required for our computation. In our case, this is the count state. On every render, we store it somewhere safe to refer back to later.
  • During a render, we can then compare the current value of count with our stored value.
    • The values are identical: Great! We don't need to perform our calculation, so we need to return our existing result, which we have conveniently also saved for later.
    • The values differ: We perform our computation, making sure to save the result so that we can access it in subsequent renders.

An implementation could look something like this:

function Counter() {
    const [count, setCount] = useState(0);
    const cache = useCache(2); // Imaginary cache hook

    let computationResult;
    if (cache[0] !== count) {
        computationResult = performExpensiveComputation(count);
        cache[0] = count;
        cache[1] = computationResult;
    } else {
        computationResult = cache[1];
    }

    return (
        <>
            <button onClick={() => setCount((prev) => prev + 1)}>
                Increment
            </button>
            <p>Count: {count}</p>
            <p>Computation Result: {computationResult}</p>
        </>
    );
}

In this code, we create some sort of cache (by using an imaginary hook called useCache) of size 2. The first item in the cache will store our first and only dependency, count. The second value in the cache stores the corresponding computation result.

We can then follow our caching logic described above to achieve manual memoization, without any help from React internals!

Introducing React Compiler

Finally, we arrive at the topic of this article. "Took you long enough!", I hear you say. Sorry.

So why did we go down the hassle of memoizing our code manually? Surely nobody would want to actually go in and write this code for real! And that's where React Compiler comes in: You don't need to write this code yourself!

React Compiler is a compiler developed by Meta, and it aims to memoize your code in the way we've just done by hand – fully automatically. This means that our examples from above, performing expensive computations and rendering expensive trees, would just work. Developers can write code with little overhead and clear intentionality, and React Compiler swoops in and makes sure that its performance doesn't suffer.

How great is that!

And what we have done above is literally what React Compiler does. If you take our initial example code and run it through React Compiler in the React Compiler Playground, you will see output that does the exact same thing we've just done, except that it looks like gibberish due to the variable names being shortened for minification purposes.

Take a look at this unaltered output from React Compiler. Our cache is a bit larger to account for other memoization happening in the component, can it's called $ instead of cache, but the underlying concept and structure is identical:

function Counter() {
    const $ = _c(10);
    const [count, setCount] = useState(0);
    let t0;
    if ($[0] !== count) {
        t0 = performExpensiveComputation(count);
        $[0] = count;
        $[1] = t0;
    } else {
        t0 = $[1];
    }
    const computationResult = t0;

    // ...
}

A word of caution

Unfortunately, as with most great news, there are some considerations and pitfalls to consider.

For one thing, React Compiler will only consider one file at a time. This means that the same third party function being called with identical arguments from two different files will not result in a shared cache; instead, the computation will be performed and cached in both callee locations.

Next, React Compiler only works if your code follows the Rules of React. This means no conditional hooks, which is rather obvious, but it also applies to less obvious things like accessing refs during render. Luckily, React Compiler will statically detect such rule violations, warn you about them, and opt out of optimizing affected components, instead of failing outright.

Last but not least, React Compiler can make mistakes. It is still in beta, and in some edge cases might introduce infinite loops or other unexpected effects in your code.

Using React Compiler yourself

If you've come this far, you're probably wondering how you can start using React Compiler yourself. The good news is that after years of it being shown but not available, React Compiler was made available to the public as an open source project in May of 2024.

It consists of three primary parts:

  • React Compiler Core, the actual compiler
  • A Babel plugin, the primary supported integration
  • An ESLint plugin, to surface issues that React Compiler might trip over

When talking about React Compiler, we typically refer to the first two of these parts. Babel still powers most React tooling today, so the babel plugin covers the wide variety of use cases out-of-the-box. Some frameworks, like Next.js, have added their own configurations to enable React compiler in an even simpler way.

Generally speaking, to install React Compiler, you need the following commands:

# Babel plugin
npm install babel-plugin-react-compiler@beta

# Eslint plugin (optional)
npm install eslint eslint-plugin-react-compiler@beta

# Runtime for React 17/18
npm install react-compiler-runtime@beta

The Eslint plugin and compiler runtime are optional. The Eslint plugin will check for violations of the Rules of React mentioned above and surface them to you. This is a great tool to be notified upfront about potential causes for ending up with un-memoized components. The compiler runtime is required if you want to use React Compiler with a React version lower than 19. React 19 already ships with the compiler runtime, so you won't need to install it there.

For most well-known frameworks, the React Compiler docs have specific instructions on how to get started with React Compiler.

For Next.js, for example, getting started is as simple as installing the Babel plugin, then turning React Compiler support on in the Next.js configuration:

const nextConfig: NextConfig = {
    experimental: {
        reactCompiler: true,
    },
};

Closing thoughts

React Compiler has been in the works for years, and the hard work of the React team has really paid off. It gives a nice glance at a React future with cleaner code and less framework-specific boilerplate. While it's still in beta, React Compiler already powers large-scale applications at Meta in production – Instagram Web is using React Compiler, for example! As for a stable release, the roadmap aims for a release candidate first, so we'll still have to wait a little longer.

So should you use it today?

I would argue that if your project can afford to take some risks, trying React Compiler today can be a great way to clean up React source code and get lots of performance improvements for free in the process. But even if you can't take these risks, the Eslint plugin is a safe bet to prepare for a quick adoption path in the future.

Thank you for reading!