Welcome to the future of React 19 Compiler, where you don’t have to manually optimize every component anymore!
Joe Savona from Meta has shared several updates on the React Compiler’s status on React conf 2024 and X (formerly Twitter). While the work is still ongoing (final state), they’re testing it internally and aim to release it by the end of 2024.
The React 19 Compiler revolutionizes component memoization, providing a more auto-caching approach to managing re-renders. But that’s just the beginning!
Understanding The New React 19 Compiler
As React developers, we’re familiar with the chronic issue of state changes that initiate too many re-renders.
The solution? Manual memoization with useMemo, useCallback, and memo are often debatable, prone to errors, and require constant upkeep!
Imagine countless hours of placing memoization hooks – a tedious optimization “dance” of time-consuming and frustrating manipulations.
This led to the birth of React Forget (former name of React Compiler), an automotive re-renders tool that remains idempotent components and keeps props and state values immutable.
React Compiler brings automatic memoization to the table — much like how we use the useMemo hook to cache components. This process applies to both react-dom and react-native React applications.
React Compiler ensures that child components only re-render if their input values change, transforming the default rendering behavior.
Many of us incorrectly assume React already works this way, but with the Compiler, it does now!
To understand the impact of the React Compiler, let’s see how React handles rendering.
Common Memo Use Case #1 – Changing State With useState React Hook
React creates a tree of components, and when a component re-renders, everything below it in that tree re-renders as well.
Imagine you have a button in your app that increases the “count” state value upon every click. There is another StaticMessage component below this button, which React should not re-render when you click this button.
import React, { useState } from 'react'; // StaticMessage component that should not re-render const StaticMessage = () => { console.log('StaticMessage re-rendered'); return <p>This message should not re-render.</p>; }; // CounterButton component that increments the counter const CounterButton = () => { const [count, setCount] = useState(0); return ( <div> <button onClick={() => setCount(count + 1)}> Increment Counter </button> <p>Counter value: {count}</p> <StaticMessage /> </div> ); }; export default CounterButton;
In the current setup (without optimization), clicking the button re-renders the whole React component tree, including StaticMessage (as a child of CounterButton).
After clicking each time, React logs a “StaticMessage rendered.” message (Examine the Browser dev tools console). That message states React has re-rendered the StaticMessage component unnecessarily because we changed the “count” state.
This has a bad effect on big components with heavy logic.
To optimize, the devs cache React code themselves and prevent it from necessary re-renders with useMemo and useCallback hooks (Read my useCallback guide).
For example, do manual memoization in React of JSX code like this:
import React, { useState } from 'react'; // StaticMessage component wrapped with React.memo to prevent re-renders const StaticMessage = React.memo(() => { console.log('StaticMessage re-rendered'); return <p>This message should not re-render.</p>; }); const CounterButton = () => { const [count, setCount] = useState(0); return ( <div> <button onClick={() => setCount(count + 1)}> Increment Counter </button> <p>Counter value: {count}</p> <StaticMessage /> </div> ); }; export default CounterButton;
Wrapping StaticMessage with “React.memo” means it will only re-render when you change some of its props, which they never do in this example.
However, this manual process is error-prone and tedious.
React Compiler optimizes it all — your applications become fast right out of the box!! It automatically rewrites and optimizes your code — no need for manual tweaks!!
This optimization functions similarly to React.memo(), but now the parent component (not you) controls whether to skip the child component re-renders.
Common Memo Use Case #2 – Complex State Loading For Web Components
Imagine you have a state object like { a, b, c } and a ParentComponent managing this state for three child components (ComponentA, ComponentB, and ComponentC). Each of these components reads different parts of the state.
If you update the state with setState({…oldValue, c: somethingNew}), EVERY React component reading/using that state will re-render.
import React, { useState } from 'react'; // Child components that read different parts of the state const ComponentA = ({ value }) => { console.log('ComponentA re-rendered'); return <p>Value of a: {value}</p>; }; const ComponentB = ({ value }) => { console.log('ComponentB re-rendered'); return <p>Value of b: {value}</p>; }; const ComponentC = ({ value }) => { console.log('ComponentC re-rendered'); return <p>Value of c: {value}</p>; }; // Parent component managing the state const ParentComponent = () => { const [state, setState] = useState({ a: 1, b: 2, c: 3 }); return ( <div> <button onClick={() => setState({ ...state, c: state.c + 1 })}> Increment c </button> <ComponentA value={state.a} /> <ComponentB value={state.b} /> <ComponentC value={state.c} /> </div> ); }; export default ParentComponent;
Without any optimization, clicking the button would cause all three components (ComponentA, ComponentB, and ComponentC) to re-render because the ParentComponent updates its state.
Now, imagine the same scenario but with the new React Compiler in action.
With the compiler, only ComponentC, which reads the updated c state prop, will re-render. ComponentA and ComponentB, which read the a and b state props, will remain unaffected and retain their memoized versions.
Here’s how you would traditionally handle this manually with React.memo:
import React, { useState } from 'react'; // Child components wrapped with React.memo to prevent unnecessary re-renders const ComponentA = React.memo(({ value }) => { console.log('ComponentA re-rendered'); return <p>Value of a: {value}</p>; }); const ComponentB = React.memo(({ value }) => { console.log('ComponentB re-rendered'); return <p>Value of b: {value}</p>; }); const ComponentC = React.memo(({ value }) => { console.log('ComponentC re-rendered'); return <p>Value of c: {value}</p>; }); const ParentComponent = () => { const [state, setState] = useState({ a: 1, b: 2, c: 3 }); return ( <div> <button onClick={() => setState({ ...state, c: state.c + 1 })}> Increment c </button> <ComponentA value={state.a} /> <ComponentB value={state.b} /> <ComponentC value={state.c} /> </div> ); }; export default ParentComponent;
By wrapping each component with a React.memo, we ensure that the component only gets to re-render whenever its corresponding props change. However, again, this is a manual process that is quite error-prone and cumbersome.
The React Compiler automates this optimization, taking off significant work from developers — no need to catch every single component!
A New Era for React Developers?
React Compiler is a game changer for developers working on large codebases.
Developers often debate whether to use useMemo and useCallback hooks for all components and props, including primitives and non-primitives.
I usually recommend implementing these (including “memo”) in real-world teams to avoid performance pitfalls, while others argue they add unnecessary runtime overhead and clutter.
With the React Compiler, you no longer need to debate these issues. It handles the memoization, allowing you to focus on building features rather than worrying about optimization.
Although some may question whether React Compiler will always make the right choices, the potential benefits are ENORMOUS.
Moreover, the compiler simplifies boilerplate code — making it cleaner and easier to read — which aligns with React’s minimalistic and flexible nature.
🎈 It’s an exciting time for the React community! The Compiler is a step forward in improving React, helping developers build UI and UX faster and more efficiently.
Compiler or Transpiler?
React Compiler transforms JSX into JavaScript, not machine code — isn’t that what a transpiler does?
Let’s take a step back to understand where the React Compiler fits.
The term “compiler” is used a bit loosely here. While it doesn’t 100% fit the strict definition of a compiler, its primary function isn’t just transforming JSX into JavaScript. Instead, it performs static analysis and optimization using techniques from traditional compilers.
Both transpilers and compilers fall under the broader category of translators. Translators convert one code form into another — from high-level to high-level (transpilers) or high-level to low-level (compilers).
Transpilers are a special type of compiler — ALL transpilers are compilers, but NOT all compilers are transpilers.
Compilers typically don’t compile directly to machine code. They often compile to an intermediate representation, which is then compiled further by a runtime (like .NET’s CLR or Java’s JRE) or a compiler backend (like GCC’s gas or LLVM’s llvm-as).
Similarly, the React Compiler translates high-level JSX into optimized JavaScript to implement optimizations and improve runtime performance (like traditional C++ or Java compilers).
In this context, the JavaScript engines in browsers (or Node.js) behave as JavaScript runtimes. Here, JavaScript is an intermediate or target language, making the term “compiler” quite fitting.
Even TypeScript, a superset of JavaScript, needs to be “compiled” into JavaScript. Babel, which many of us use daily, also calls itself a compiler!
The new React Compiler goes beyond merely converting JSX to JavaScript. Its main purpose is to perform static analysis and apply optimizations during the build process.
Will React Compiler Optimize <Context>?
The short answer is yes, but “indirectly”! Fundamentally, React <Context> behavior itself won’t change.
When you provide a new context value, every component consuming that value will re-render, even if they only need a portion of the data. This is how context operates in React.
New React Compiler optimizes the output of the consuming components by comparing element references to memoize/cache their results. The child components will ONLY re-render if you change the specific data the child consumes.
This optimization means React improves performance, re-rendering fewer nested child components.
Should React Compiler Effect (Or Replace) Redux?
The short answer: not really.
To understand this better, I remind you how React and Redux function.
React Compiler optimizes behavior inside a component ONLY during its rendering phase.
On the other hand, Redux manages the state and store subscription process outside of React’s rendering lifecycle.
You also need tools like Reselect (a popular Redux library) to optimize Redux selectors and ensure the right data gets to the right components efficiently.
React Compiler optimizes what happens within the component (render), while Reselect ensures that your state changes propagate only where you need it, preventing unnecessary updates and re-renders.
Together, they help maintain performant global stores.
Possible Drawbacks Of New Features
As exciting as the new React Compiler, it has some potential pitfalls.
It might well feel that REintroducing Babel into the build pipeline is a step backward — especially if you are using ESBuild or SWC.
If you are one of those developers, you have my sympathies. It’s like swapping out your sleek sports car for a simple family sedan — you can use it, but it doesn’t give you the same thrill.
As it stands, adopting React Compiler seems like a no-go for many. The community is buzzing with anticipation, but there’s also a noticeable sense of caution….
…Let’s “wait and see” how this new tool performs over the coming months!
On the surface, it sounds like a dream come true: “The compiler does ALL the memoization for you.”
But here’s the hitch — not all of the memoization is helpful. Automatically, in some cases, memoization could use undue memory. This is true when it caches far too large or unnecessary (or both, lol) data.
It might be wiser to learn how to manually control memoization (components caching) and use useMemo and useCallback hooks instead of diving headfirst into the React Compiler.
Think of it like driving — knowing how to shift gears manually makes you a better driver, even if you usually drive automatically.
Next and Turbo Compiler
If you are wondering, “Will Next JS use React Compiler?”, the wait is almost over!
Next.js introduced an experimental flag to enable React Compiler with an experimental configuration to set up Babel with babel-plugin-react-compiler (Also compatible with Vite, Remix, Webpack, and Metro for React Native).
But Turbo Compiler (The compiler that the Next Js team is working on) is still on the horizon. The development process and DX will significantly boost with its upcoming release (anticipated soon).
One of the most common complaints about Next.js is the slow dev server. The Turbo Compiler aims to address this issue, promising faster builds and a more efficient development workflow.
Some concerns about the choice of Rust as the main language for Turbo, but we can only hope that the final product justifies this decision.
Next.js and React — A Synergistic Growth Of Server Components
There is a lot of buzz in the Next.js community about its close collaboration with the React team. Some believe that Next.js is driving significant changes in React, but this perspective is somewhat mistaken.
React was always more than a client-only library. From the beginning, it was designed with a full-stack mindset, with the server-side components in mind.
Next.js alignes itself closely with React, more than any other framework.
While React sets the architectural design and proposes new features, Next.js (
a key player in the React ecosystem) implements them upon full approval.
React vs Svelte
The landscape of front-end frameworks is ever-evolving, and the latest developments in React and Svelte showcase this dynamic.
React is moving towards a compiler, while Svelte adds its new runes syntax in Svelte 5, similar to React useState hook.
The reality is that every framework and library learns from others and will continue to do so. It’s all JavaScript at the end of the day — the best choice depends on your specific needs and project requirements.
But the basic difference between React and Svelte is at the very core of their design, which still remains distinct.
Svelte was built with a compiler-first approach, processing components at build time rather than runtime. This design pattern gives Svelte a significant performance advantage.
React Compiler (while a step in the right direction) can’t fully replicate this ground-up approach. Performance-wise, it does not really change React’s core architecture, which relies heavily on a virtual DOM and runtime diffing algorithm.
Should You Still Learn Manual Memoization in React?
If React Compiler optimizes our code automatically, why bother with these manual useMemo and useCallback hooks?
Similarly to class components, manual caching becomes a must-have knowledge working with old React codebases.
Knowing how to use useMemo, useCallback, and memo will help you understand and improve performance in legacy applications.
Compilers can only work with what you give them. If your code is inefficient — expecting the compiler to work miracles is unrealistic.
Understand how to optimize code and use compiler-friendly syntax to allow React Compiler to enhance your code.
The React Compiler will (undoubtedly) make DX better, but there will always be edge cases where your knowledge of React comes in handy.
Conclusion: Compiler and React 19 Bring Auto Memoization
React 19 Compiler represents a huge milestone in the evolution of React, focusing on enhancing DX, user experience and app performance.
This focus remains on efficient components that indicate a commitment to enhancing DX and application performance.
React’s future is bright — React 19 Compiler is proof of the continuous innovation in React ecosystem.