什么是React Compiler
要了解 React Compiler,这还需要从 React 的更新机制说起。当React项目中某个组件的state状态发生变更,react更新机制就会从该组件开始往下进行对比,通过双缓存机制判断哪些节点发生了变化,然后更新节点。这样就会导致只要父组件渲染了,子组件也会重新渲染,这样的更新机制成本并不小,因为在判断过程中,如果 React 发现props,state,context任意一个不同,那么就认为该节点被更新了。因此,冗余的 re-render 在这个过程中会大量发生
优化策略
在这样的背景下,冗余的 re-render 在项目中大量发生,这也是React被吐槽性能不好的主要原因。因此React提供了useMemo、useCallback、memo可以缓存某些节点,但这需要开发者对于这几个缓存的API有深刻且准确的了解。有的开发者虽然在项目中大量使用了,但是未必就达到了理想的效果。React Compiler 则是为了解决这个问题,它可以自动帮助我们记忆已经存在、并且没有发生更新的组件,从而解决组件冗余 re-render 的问题
对我们的开发有没有影响
React Compiler 是被集成在代码自动编译中,因此只要我们在项目中引入成功,就不再需要关注它的存在。我们的开发方式不会发生任何改变。 它不会更改 React 现有的开发范式和更新方式,侵入性非常弱
React Forget
检测兼容性
检测我们现有的代码是否兼容React Compiler
前提
并非所有的组件都能被优化,因此早在 React 18 的版本中,React 官方团队就提前发布了严格模式。在顶层根节点中,套一层 StrictMode 即可。遵循严格模式的规范,我们的组件更容易符合 React Compiler 的优化规则
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
怎样检测
在项目根目录下执行如下指令
npx react-compiler-healthcheck
该脚本主要用于检测
- 项目中有多少组件可以成功优化 :越多越好
- 是否使用严格模式,使用了优化成功率更高
- 是否使用了与 Compiler 不兼容的三方库
在项目中引入
支持部分目录运行 Compiler
由于 JavaScript 的灵活性,Compiler 无法捕获所有可能的意外行为,甚至编译之后还会出现错误。因此,目前而言,Compiler 依然可能会有他粗糙的一面。因此,我们可以通过配置,在具体的某一个小目录中运行 Compiler
const ReactCompilerConfig = {
sources: (filename) => {
return filename.indexOf('src/test') !== -1;
},
};
支持 eslint 插件
React Compiler 还支持对应的 eslint 插件。该插件可以独立运行。不用非得与 Compiler 一起使用
安装
npm i eslint-plugin-react-compiler
在 eslint 的配置中添加
module.exports = {
plugins: [
'eslint-plugin-react-compiler',
],
rules: {
'react-compiler/react-compiler': "error",
},
}
用法
Compiler 目前结合 Babel 插件一起使用,因此,我们首先需要在项目中引入该插件
npm install babel-plugin-react-compiler
安装后,将其添加到你的 Babel 配置中
👀注意:该插件应该在其他Babel之前运行
// babel.config.js
const ReactCompilerConfig = { /* ... */ };
module.exports = function () {
return {
plugins: [
['babel-plugin-react-compiler', ReactCompilerConfig], // must run first!
// ...
],
};
};
在 vite 中使用
首先,我们需要安装 @vitejs/plugin-react ,v4.2.1之后的版本有问题,所以安装4.2.1版本即可。然后在vite.config.js中添加如下配置
const ReactCompilerConfig = {};
export default defineConfig(() => {
return {
plugins: [
react({
babel: {
plugins: [
["babel-plugin-react-compiler", ReactCompilerConfig],
],
},
}),
],
// ...
};
});
实践
React 版本要求
react-compiler-runtime库依赖于React 19,为了能和React 18 一起使用,有两种解决方式
- 实现该库中的 c 函数
- 在package.json中配置
// polyfill
import { useState } from "react";
export function c(size) {
return useState(() => new Array(size))[0];
}
// 修改package.json中配的配置
https://github.com/undesicimo/r17-with-compiler/blob/main/package.json
配置vite.config.js
const ReactCompilerConfig = {
runtimeModule: '@/mycache'
};
export default defineConfig(() => {
return {
plugins: [
react({
babel: {
plugins: [
["babel-plugin-react-compiler", ReactCompilerConfig],
],
},
}),
],
};
});
对比
// 测试代码
const B = () => {
console.log('B render')
return (
<div>
我是 B 组件
</div>
)
}
const A = () => {
const [counter, setCounter] = useState(0)
console.log('A render')
return (
<div style={{ padding: 100 }}>
<button onClick={() => setCounter(counter + 1)}>
点击修改
</button>
counter:{counter}
<B />
</div>
)
}
未使用Compiler
当 A 组件的 state 发生改变 ,B组件也会渲染,尽管 B 组件没有用到 A组件中的state或者props 或者context,B 组件也会重新渲染
使用Compiler
当 A 组件渲染的时候,B 组件没有重新渲染了
分析
React Compiler 编译之后的代码并非是在合适的时机使用 useMemo、memo等 API来缓存组件,而是使用了一个名为 useMemoCache 的 hook 来缓存代码片段
Compiler 会分析所有可能存在的返回结果,并且把每个返回结果存储在 useMemoCache 中。在这个例子中,编译器知道需要用 9 个长度的数组来保存,第一个位置存储初始化标志,后面的位置用于存储state、setState以及具有DOM 树的 jsx 的记忆版本。如果判断之后发现该结果可以复用,则直接通过读取序列的方式使用即可,比起之前链表存储结果,执行效率上会高不少
// 编译后的 A 组件
// export function c(size) {
// return useState(() => new Array(size))[0];
// }
import { c as _c } from "/src/mycache/index.ts";
const A = () => {
_s();
const
$ = _c(9);
if ($
[0] !== "42701a5839b43d9af82882bafb131108d07937c6db852d9299461b77c362838e") {
for (let
$i = 0; $
i < 9;
$i += 1) {
$
[
$i] = Symbol.for("react.memo_cache_sentinel");
}
$
[0] = "42701a5839b43d9af82882bafb131108d07937c6db852d9299461b77c362838e";
}
const [counter, setCounter] = useState(0);
console.log("A render");
let t0;
if (
$[1] !== counter) {
t0 = () => setCounter(counter + 1);
$
[1] = counter;
$[2] = t0;
} else {
t0 = $
[2];
}
let t1;
if (
$[3] !== t0 || $
[4] !== counter) {
t1 = jsxDEV("button", { onClick: t0, children: [
"点击修改 counter:",
counter
] }, void 0, true, {
fileName: "/Users/ivy/zaihui/tiktok-1/src/App.tsx",
lineNumber: 30,
columnNumber: 134
}, this);
$[3] = t0;
$
[4] = counter;
$[5] = t1;
} else {
t1 = $
[5];
}
let t2;
if (
$[6] === Symbol.for("react.memo_cache_sentinel")) {
t2 = jsxDEV(B, {}, void 0, false, {
fileName: "/Users/ivy/zaihui/tiktok-1/src/App.tsx",
lineNumber: 33,
columnNumber: 10
}, this);
$
[6] = t2;
} else {
t2 =
$[6];
}
let t3;
if ($
[7] !== t1) {
t3 = jsxDEV("div", { children: [
t1,
t2
] }, void 0, true, {
fileName: "/Users/ivy/zaihui/tiktok-1/src/App.tsx",
lineNumber: 33,
columnNumber: 75
}, this);
$[7] = t1;
$
[8] = t3;
} else {
t3 = $[8];
}
return t3;
};
因此,编译之后的代码看上去更加繁杂,但是执行却更加高效