React unnecessarily re-renders many components because it is purely runtime-based, unlike other frameworks like [Vue](https://vuejs.org/guide/introduction.html), [Svelte](https://svelte.dev/docs), and [Solid](https://www.solidjs.com/docs/latest). This article will explain how the React compiler solves this by handling the automatic memoization of code for efficiency, reducing unnecessary re-rendering and boosting application performance.
Session Replay for Developers
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — an open-source session replay suite for developers. It can be self-hosted in minutes, giving you complete control over your customer data.
Happy debugging! Try using OpenReplay today.
Let's see how the React compiler works, how it’s different from traditional memoization, and how to use it in your projects. When the project is built, the compiler creates code that memoizes the components. When the app re-renders, the code generated by that compiler checks to see parts of the component without state changes and then returns the memoized code. This ensures precision when it updates with no additional effort on the developer's part and well-written code.
To properly understand how the compiler works, let's examine the current Babel transpiler. Run the code below in the REPL to see the transpiler's output.
export default function Hello() {
return(
<div className="foo">Hello World </div>
);
}
You should see this output below. The transpiler converts the JSX in the function to React's jsx
runtime function. The JSX is converted into jsx("div", { className: "foo", children: "Hello World" })
. With this setup, a new React element is created every time the Hello() function is rendered. There’s no built-in memoization, which leads to unnecessary re-renders that adversely affect the app’s performance.
import { jsx as _jsx } from "react/jsx-runtime";
export default function Hello() {
return /*#__PURE__*/_jsx("div", {
className: "foo",
children: "Hello World "
});
}
When you compare this to the React Compiler’s output for the same code in its REPL here, you get this output.
function Hello() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <div className="foo">Hello World </div>;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
Here, it introduces a caching mechanism Symbol.for("react.memo_cache_sentinel"))
to know when to use a cached version of the function. The $
variable is an array used to store cached values. The compiler would check if a cache exists before it re-renders. So, it only re-renders parts of the function when it is necessary.
Comparison with Existing Memoization Techniques
React Compiler simplifies memoization, but let’s compare it to the approaches that existed earlier, such as React.memo
, useMemo
, and useCallback
.
Feature | React.memo | useMemo | useCallback | React Compiler |
---|---|---|---|---|
Automatic vs. Manual | Manual | Manual | Manual | Automatic |
Granularity | Component Level | Value Level | Function Level | Integrated into rendering |
Ease of Use | Requires explicit use | Requires explicit use | Requires explicit use | Built-in, less developer effort |
Dependency Management | Managed by developer | Managed by developer | Managed by developer | Managed by compiler |
Performance Impact | Significant if used correctly | Significant if used correctly | Significant if used correctly | Consistent performance gains |
Potential for Bugs | Higher due to manual control | Higher due to manual control | Higher due to manual control | Lower due to automatic control |
Example Syntax | React.memo(MyComponent) |
useMemo(() => fn, [deps]) |
useCallback(() => fn, [deps]) |
None. The compiler handles it |
Some benefits of automatic memoization are:
- Simplicity: It would make codes simpler and elegant as developers would not need to manually apply memoization using
React.memo
,useMemo
, oruseCallback
hooks - Performance: It would improve app performance by reducing unnecessary re-renders. This would be most useful for complex apps where manual memoization can be challenging.
It is important to note that when the compiler sees code that isn’t well-written in React, it defaults to the original transpiler.
Demonstrating React App Before Compilation
In this section, we will demonstrate how the React app behaves before the compiler optimizes it. Observing the difference during run time allows us to spot some inefficiencies, such as unnecessary rendering. It would make us appreciate the improvement made by the React compiler. Let's examine the code below:
function CustomHeader() {
console.log("CustomHeader is re-rendering");
return (
<header>
<h1>Custom Counter</h1>
</header>
);
}
function CustomCounter() {
const [count, setCount] = useState(0);
return (
<>
<CustomHeader />
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
</>
);
}
export default CustomCounter;
This is a React app in which the transpiler re-renders the whole component, even with components that don’t have state changes. The CustomHeader
function in this code doesn’t change, but it re-renders. We see this through the console whenever we click the increment button; it logs "CustomHeader is re-rendering" when the component re-renders.
Image of the console showing the CustomHeader
function continuously re-rendering without any state changes.
We could fix this by using useMemo
to memoize the code. It would watch the code for state changes and only render the component when there is a state change. Below are the changes we would make to the code.
function CustomHeader() {
console.log("CustomHeader is re-rendering");
return (
<header>
<h1>Custom Counter</h1>
</header>
);
}
function CustomCounter() {
const [count, setCount] = useState(0);
// Memoize the JSX for the header
const headerJSX = useMemo(() => <CustomHeader />, []);
return (
<>
{headerJSX}
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
</>
);
}
export default CustomCounter;
In this code, we wrap the CustomHeader
in a useMemo
hook. When we look at the console, this component only renders once, even after clicking the increment button multiple times.
A preview of the console showing the CustomHeader
component renders once and doesn’t re-render even when the increment button is clicked multiple times.
Demonstration with the React Compiler
When we run our initial code in the compiler, it doesn’t re-render the CustomHeader
component. Instead, it automatically memoizes the code and renders it once, just as it did when we used the useMemo()
hook.
You can check this yourself by following these steps to install the React compiler.
- First, install
Vite
by running the code below. Then, select the React option and Javascript.
npm create vite@latest .
- Install React 19.
npm i react@rc react-dom@rc
You can confirm the version in the package.json
file.
- Next, install the React compiler.
npm add babel-plugin-react-compiler
- Open the
vite.config.js
file and add these configurations for the React compiler to enable it to work.
const ReactCompilerConfig = { /* ... */ };
export default defineConfig(() => {
return {
plugins: [
react({
babel: {
plugins: [
["babel-plugin-react-compiler", ReactCompilerConfig],
],
},
}),
],
};
});
The compiler is set up. Next, we paste our initial code into the App.jsx
file and run it. If it is set up correctly, the CustomHeader
only renders once, as seen in the console. This shows that the compiler automatically memoizes the code without the useMemo()
hook we used for the traditional method.
The image shows the console only rendering the CustomHeader
function once, even when we continually click on the increment button because of the compiler.
Can you run into errors while using the compiler?
You can encounter errors when using the React compiler. These errors can result from three main issues: errors arising from violating React's rules, infinite loops during runtime, and build-time errors.
Using the eslint-plugin-react-compiler
will display any violations of the rules of React in your editor. If a component violates this rule, the compiler can skip it and try to optimize the following components.
Let's examine a code example that violates a rule in React and observe the output in the REPL.
import React, { useState, useEffect } from 'react';
function useConditionalHook(shouldUseEffect) {
const [count, setCount] = useState(0);
if (shouldUseEffect) {
useEffect(() => {
console.log('Effect is running');
// This effect will only run if shouldUseEffect is true
}, []);
}
return [count, setCount];
}
function App() {
const [count, setCount] = useConditionalHook(true);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default App;
In the code above, the hook is called conditionally, which goes against the rule in React. Hooks should not be called conditionally; they should be called in a consistent order. This error is fired off by the eslint-plugin-react-compiler
, as observed in the REPL output below.
The image below shows the error in the React compiler playground when the React cod
However it is important to note that without the eslint-plugin-react-compiler
, the code above would run in React 19 uses concurrent mode as the default rendering mode. In this mode, React can safely handle conditional hook calls. It is still essential to follow the rules of React even though React 19 is forgiving.
Conclusion
React 19's introduction of a compiler has helped address the inefficiencies of the previous Babel transpiler. This handles unnecessary re-rendering by introducing a cache system, making the code simpler, and removing errors introduced by manual implementation. This generally improves the app’s performance and efficiency.
Top comments (0)