Setup and Dive into the React 19 Compiler Optimizations

The recent React 19 Compiler has been shown during React Conf 2024. It introduces tremendous performance improvements into react component rendernig process. And can be plugged in into existing codebases.

In this video we analyze the output of transpiling a simple react component with and without the compiler. We also analyze how exactly does react introduce the optimizations. Lots of interesting facts covered

Share with a coworker

Social Share Links

Send Tweet

Transcript

[00:00:00] The new React compiler is awesome. Let's see what it does by running a new Vite based React TypeScript application. And let's compare what is the difference between the output before using the compiler and after. Let's start by installing the dependencies and running the application. So

[00:00:20] I'll run npm install and npm run dev. Our application is up and running as we can see so let's start with walking through the React 19 beta upgrades. So let's take a look at the installation guide. So we need React and React DOM in version beta. So let's run quickly

[00:00:40] and npm install for the proper versions. And Let's also see how it looks in package.json. So here we can see a snapshot of a certain date of React 19. So that's 1 thing. Another thing is that we need to upgrade the types for React 19 since there is quite a bit of new things

[00:01:01] available within the public API of the package. We also need to update the overrides within package.json. So that's 1 thing. We've got React 19 and that's it. Let's also take a look at the setup guide for the React compiler. So what we need to do first is just to

[00:01:21] make sure whether everything is ready within our setup for running React compiler. So we're running npx react-compiler-health-check. This is going to be okay. So successfully compiled 1 out of 1 component since we have only 1 component, which is basically app TSX, just a pure Vite install. So

[00:01:41] strict mode usage is found within index TSX and found no usage of incompatible libraries, well, since this is just a clean install. So everything is ready to install the compiler. So let's start with installing the ESLint plugin for the compiler, and let's update the

[00:02:01] ESLint configuration. So first, we're going to add a plugin over here and we would add a compiler related rule to the set of rule that we have got here. So this is done. The second thing we need is to install the

[00:02:21] Babel plugin which is going to be the 1 that is going to fuel our application. So let's open Vite config TS and the thing that we want to replace is as we can see here usage with Vite is to modify the definition for React over here so we are going

[00:02:41] to replace this empty React 1 with Babel plugin this Babel plugin React compiler Now we don't want to overwrite the default configuration so we'll just leave it as it is and this should be already it. We should not require anything apart from that. So

[00:03:01] let's walk back to our application and let's take a look at the console output. So I'm going to minimize the code part and let's open some dev tools now. When we open the components dev tools, we'll see this magical sign, this magical tool tip saying this component has been auto-memorized

[00:03:22] By the React compiler. So of course obviously if we take a look at app TSX we will see that this is simply just a component. This is an export default so there is no memo around it. So this has been automatically memoized. So 1 of the things that React compiler is doing is automatically memoizing

[00:03:42] the components. Nice thing about Vite is that we can modify the configuration of Vite without having to kill the server and restarting it. So I can simply just comment out Babel and this will make the browser refresh. We'll basically see that now without this transpiler, this compiler, we

[00:04:02] can see that the component is not automatically memoized. And again, let's just bring back the Bubble plugin and after the refresh we can see, yep, it's back again. So this is our setup. Let's take a look at the content of the app.tsx file, so we will trim it down a little bit. We don't

[00:04:23] need most of these things over here, so let's just put the button and this is what we're going to display. We're also going to display a square. So a square is just going to be the default count which we are going to just make x

[00:04:43] asterisk asterisk, sorry, count asterisk asterisk times 2 and we're just going to simply display that the square is just whatever. Like I want to keep things simple so that it will be easier for us when we're actually going to analyze the output of the compiler. So everything

[00:05:03] is up and running. Now we are going to take a look at the sources so I'll copy the content of the app component and let's take a look at the Babel REPL just to see how would a compiled, just transpiled from JSX into basically

[00:05:23] just pure JS, how would this look like originally without the Babel compiler. So I'll just put what is the original pre-compiled version. This is the post compilation so we can see that there is basically the old former React.createElement function call so this is basically

[00:05:44] just made shorter. So this is the 1 without the compiler. And let's take a look how our application again would look like if we turned this Babel plugin off. And let's again open the DevTools and Let's take a look at the sources. So we've got the app TSX, which is

[00:06:04] basically our Application content. This is our original source code And when we take a look at the app TSX Then we can clearly see that this is pretty much the same thing so we can see this is basically function app and this is function app this is const count set count equals

[00:06:24] use state of 0 here we can just see the square equals count etc so here it says JSX dev fragment and children Here we've got pretty much the same, just this 1, the name is just different, but all the rest is pretty much almost the same. Just we have some additional output like the file name, line number,

[00:06:44] etc. Just for the source maps to work. But other than that, this is generally how React used to be transpiled. So let's turn the React compiler on. Now this is going to be totally different. So here we've got function app and now comes the interesting part. So we've got the dollar

[00:07:05] which runs the underscore C with 5. So 5 is basically the size of the compiler's temporary memory, the cache for a component. Here we've got a lot of ifs and for statements so here we do have the useState but again we're going to

[00:07:25] look at quite a lot of these if statements with the symbol.for ReactMemoCacheSentinel So generally what is going on is that this underscore C function which you could by the way find out whenever you want to use React 19 features and you haven't installed

[00:07:46] React 19 so for instance if you're running React 18 the previous version and you would like some compiler output then basically you're going to get an error saying that this underscore C is not defined this basically means that you have forgotten to update your versions. So what is going on here is that

[00:08:06] within a single component we need to create a little bit of memory a little bit of empty space for the compiler to cache the data. Here we've got essentially initializing this cache so we're going to move from 0 up to 4 which is basically

[00:08:27] feeding the values over here and as you can see there is a dollar sign over here that we are actually feeding, right? So this is just an array of a certain size that we are just pre-feeding with a symbol which is basically going to denote that initially this hasn't been

[00:08:47] initialized in any way by any value, so this is kind of an empty placeholder. And when the actual rendering takes place, then initially, our space is basically Fed by this placeholder. So we are going to check whether hey, is this the first time that you're actually running

[00:09:07] the thing? So if this is the first time then we are actually going to put like what is Actually going to be the callback Now if we have created it then we're just putting it on the index 1 or whatever the index is there so we're just feeding the cache that we have created here. Now

[00:09:27] if it's not the first time then we can rest assured that this is already there so there is no need to do it. So we can see that actually thanks to the React compiler we are the runtime, React's runtime is going to do way less operations in the runtime. So what you could have known from old React is that on each

[00:09:47] render of this component there would be a new callback being not only created but also pushed into this button event handler for the click event. And now we can see that no it's not the case because the compiler is inlining the function so that it is essentially and actually created

[00:10:08] only on the first shot only on the first render because there is no need to recreate the functions over and over again. Let's also take a look at what is actually this underscore C function and for this reason let's walk into the React working group, React compiler, 1 of the discussions and we will see what is

[00:10:28] actually an example polyfill. Of course this is not what the implementation that is being used, but it Nicely illustrates what is going on so this is the use memo cache, so this is the size of the cache So we're just doing react.useState So we are initializing some empty space

[00:10:49] as we've said just the array and we are Initializing it with whatever is the empty symbol in this case. And this is our React memo cache sentinel. So this is the what we've called placeholder. So well technically it is initialized but it denotes kind of nobody has put any actual

[00:11:09] value inside of it. So we can treat it like logically as being empty. Also, we are putting that under this symbol, this is true, so whenever we are going to treat the symbol as a key, in order to use the value, most probably, and that's the whole point of the symbols,

[00:11:29] we need to have the reference to the symbol. So basically, even if it's there, then if somebody doesn't have the symbol then we cannot access it so for this reason this basically remains as an implementation detail of React's runtime so that nobody can interfere with that, nobody can break it. So what is important here

[00:11:49] is that whatever variables, whatever expressions we might have within our component, the size of the cache for a single component is going to be updated accordingly. So in this case, whether something is a simple expression just like the count or a square or even the callback that we're

[00:12:09] going to put into the button, this is just going to be 1 of the elements that is going to feed the cache. But what is important is that memoization is not only limited down to count, square and other expressions, it also includes memoizing the template itself. This is also an expression that we're going to put into

[00:12:30] our JSX DOM, whatever. So we can see over here that well actually what does this text node, the future text node, depend on? This is the static part, this is the dynamic part, the count variable, square is another static part and the square variable is the dynamic part. So what we've got here

[00:12:50] is yet another piece that could be cached. This is basically T1. So if our cache, our placeholder for whatever count could be or the placeholder with index 3 whatever square could be if these are not the same so either you haven't yet executed a

[00:13:10] thing or the values changed so this should remind you as use memo then basically rerun whatever is this piece here so we're not only creating a fiber node but we are also building the string out of this countIs, squareIs etc. There is

[00:13:30] also quite a bit of the internal react attributes that we need to pass over here But still if either we are doing it initially Like in use memo or any of the dependencies for this text node has changed again same as with use memo we're just going to recreate it. Also we are going to cache what

[00:13:50] are the dependencies that we want to compare later on. And finally, when the whole thing has been already calculated, and that is in this case T1, then we are again going to cache it. But if we find out that there is a re-render of this component and the cache index 2

[00:14:10] is basically count and cache index 3 is basically square, so what are we going to do? We don't have to recompute the cache index for because it's already there so it didn't invalidate so we can basically grab it. So as we can see this compiler is kind of making a mix of both useMemo,

[00:14:31] use callback and actually even inlining the compilation of pieces of the template. So if we look back at the document over here, then we can see that React compiler automatically memoizes values but does not use useMemo or useCallback. Why is that? Because useMemo

[00:14:51] or useCallback, they come with their own overhead. For instance, each time they would have to recreate the function. Maybe they would use it, maybe not, but actually creating the function also takes time and takes resources so inlining is going to be way more effective. And as you can see the compiler is

[00:15:11] going to make the entire rendering process of each component way faster and way cheaper.