注意 React Compiler 目前仍处于实验阶段,需要 React 19 Beta 不建议在生产中使用~。(可观看在 React Conf 中的介绍)
使用场景(为什么要使用?)
在平时开发中,我们经常会因为避免不必要或不是预期的 rerender(props、state、context) 或缓存一些计算结果,而使用 React.memo、useMemo、useCallback 等记忆函数,例如:
props 变化导致的 rerender
当父组件 rerender 后,若子组件未使用 React.memo,则子组件也会 rerender。然而当子组件使用 React.memo 后,若 props 中的某个属性为一个匿名函数或对象时,子组件仍会 rerender
const ChildComponent = (props) => {
const { count, onChange, config } = props;
console.log('render', config);
return (
<div>
<button onClick={onChange}>click</button>
<span>{count}</span>
</div>
);
};
const Page = () => {
const [count, setCount] = useState(0);
const [number, setNumber] = useState(0);
return (
<div>
<ChildComponent
count={count}
onChange={() => console.log(123)}
config={{ someConfig: 'some cinfig here' }}
/>
</div>
);
};
手动避免 rerender
为什么使用了 memo 还会触发 render?React 中的 memo 依赖相关比较都是浅比较,当 props 中的某个属性为一个匿名函数或对象时,memo 会认为 props 发生了变化,从而触发 render。
const objectIs: (x: any, y: any) => boolean =
// $FlowFixMe[method-unbinding]
typeof Object.is === 'function' ? Object.is : is;
function areHookInputsEqual(nextDeps, prevDeps) {
//...
// $FlowFixMe[incompatible-use] found when upgrading Flow
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
// $FlowFixMe[incompatible-use] found when upgrading Flow
if (objectIs(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
所以,我们在使用 React.memo 的同时,也需要在父组件中使用 useMemo 或 useCallback 来避免不必要的 render。
import { useMemo, useCallback, memo } from 'react';
const ChildComponent = memo((props) => {
const { count, onChange, config } = props;
console.log('render', config);
return (
<div>
<button onClick={onChange}>click</button>
<span>{count}</span>
</div>
);
});
const Page = () => {
const [count, setCount] = useState(0);
const [number, setNumber] = useState(0);
const handleChange = useCallback(() => {
console.log(123);
}, []);
const config = useMemo(() => {
return { someConfig: 'some cinfig here' };
}, []);
return (
<div>
<ChildComponent count={count} onChange={handleChange} config={config} />
</div>
);
};
Checking compatibility - 检查老项目是否兼容使用 React Compiler
在上述描述后,或者平时在开发中早已被这些 “Memo” 产生了无形的性能焦虑。可以使用 react 提供的 healthcheck 在已有项目中尝试~ 可以先检查代码是否兼容。
npx react-compiler-healthcheck@latest
该脚本将会检查:
- 多少组件可以成功优化
- 检查是否使用了
<StrictMode>,启用<StrictMode>后,编译器的优化将更为轻松,说明你的项目已经在遵循 React 规则 - 检查不兼容的库使用。
Successfully compiled 8 out of 9 components.
StrictMode usage not found.
Found no usage of incompatible libraries.
配置 eslint-plugin-react-compiler
React Compiler 提供了 eslint 插件,用于检查代码是否符合优化规则,且独立于 React Compiler。当该插件显示你的代码有违反 React Rules 时,编译器同样也会跳过优化。
例如,修改 props 的值。
pnpm install eslint-plugin-react-compiler
配置 .eslintrc.js
暂未提供 9.x
.eslintrc.js
module.exports = { plugins: ['eslint-plugin-react-compiler'], rules: { 'react-compiler/react-compiler': 'error', }, };
配置 react compiler
sources
在指定文件夹中优化。
const ReactCompilerConfig = {
sources: (filename) => {
return filename.indexOf('src/path/to/dir') !== -1;
},
};
compilationMode
支持 annotation、infer、syntax、all,当你只需要在某些组件或 hooks 中优化时,可以使用 annotation 模式,并在组件或 hooks 中添加 use memo 注释。
const ReactCompilerConfig = {
compilationMode: 'annotation', // annotation | infer | syntax | all
};
// src/app.jsx
export default function App() {
'use memo';
// ...
}
或者只需要反选某些不需要的组件。需要注意的是,注释模式并不在长久计划内。
const ReactCompilerConfig = {
compilationMode: 'all', // annotation | infer | syntax | all
};
// src/app.jsx
export default function App() {
'use no memo';
// ...
}
logger
目前该属性并没有明确的文档说明,但通过源码解读时,发现可以通过该属性来定义日志输出。
const ReactCompilerConfig = {
logger: {
logEvent: (fileName, event) => {
console.log(fileName, event, 'reactCompiler');
},
},
};
在现有项目中使用 React Compiler
React Compiler 提供了 Babel 插件,将其添加至 babel 配置中即可。注意,插件需要在其他插件之前运行。
pnpm install babel-plugin-react-compiler
babel.config.js
const ReactCompilerConfig = {}; module.exports = function () { return { plugins: [ ['babel-plugin-react-compiler', ReactCompilerConfig], // must run first! // ... ], }; };
配置成功后,在运行或构建时。
在 Next 中使用 React Compiler
在 Next RC 15 中,支持 React Compiler,升级后,只需要在 next.config.js 中添加 reactCompiler 配置即可。
pnpm install next@canary react@canary react-dom@canary babel-plugin-react-compiler
next.config.ts
const nextConfig = { experimental: { // reactCompiler: true, reactCompiler: { compilationMode: 'annotation', }, }, }; module.exports = nextConfig;
配置成功后,可在 React Devtools 中发现 Memo 标志
源码浅析
function ChildComponent(props) {
const { count, onChange, config } = props;
console.log('render', config);
return (
<div>
<button onClick={onChange}>click</button>
<span>{count}</span>
</div>
);
}
function Page() {
const [count, setCount] = useState(0);
const [number, setNumber] = useState(0);
return (
<div>
<ChildComponent
count={count}
onChange={() => console.log(123)}
config={{ someConfig: 'some cinfig here' }}
/>
</div>
);
}
function ChildComponent(props) {
const $ = _c(7);
const { count, onChange, config } = props;
console.log('render', config);
let t0;
if ($[0] !== onChange) {
t0 = <button onClick={onChange}>click</button>;
$[0] = onChange;
$[1] = t0;
} else {
t0 = $[1];
}
let t1;
if ($[2] !== count) {
t1 = <span>{count}</span>;
$[2] = count;
$[3] = t1;
} else {
t1 = $[3];
}
let t2;
if ($[4] !== t0 || $[5] !== t1) {
t2 = (
<div>
{t0}
{t1}
</div>
);
$[4] = t0;
$[5] = t1;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
function Page() {
const $ = _c(4);
const [count] = useState(0);
useState(0);
let t0;
let t1;
if ($[0] === Symbol.for('react.memo_cache_sentinel')) {
t0 = () => console.log(123);
t1 = {
someConfig: 'some cinfig here',
};
$[0] = t0;
$[1] = t1;
} else {
t0 = $[0];
t1 = $[1];
}
let t2;
if ($[2] !== count) {
t2 = (
<div>
<ChildComponent count={count} onChange={t0} config={t1} />
</div>
);
$[2] = count;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
解析
React Compiler 并不是对 props 或相关变量增加 useMemo 或 useCallback,而是将 props 或相关变量的值缓存至一个变量中(在测试时发现同一个 变量 被存了多份,暂不知是设计如此还是?),当值发生变化时,重新渲染,反之取缓存。
与其他优化相比,使用 Babel 后的 React Compiler 实现了更细粒度的优化,能精确地实现最小化更新。
react/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
function compileProgram(program: NodePath<t.Program>, pass: CompilerPass) { const useMemoCacheIdentifier = program.scope.generateUidIdentifier('c'); // .... compiledFn = compileFn( fn, config, fnType, useMemoCacheIdentifier.name, pass.opts.logger, pass.filename, pass.code, ); }
react/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts
function codegenFunction(fn: ReactiveFunction, uniqueIdentifiers: Set<string>) { // .... // 共缓存的变量 数量 => // const $ = _c(4); // t0 = $[0]; const cacheCount = compiled.memoSlotsUsed; // The import declaration for `useMemoCache` is inserted in the Babel plugin preface.push( t.variableDeclaration('const', [ t.variableDeclarator( t.identifier(cx.synthesizeName('$')), t.callExpression(t.identifier(fn.env.useMemoCacheIdentifier), [ t.numericLiteral(cacheCount), ]), ), ]), ); if (fastRefreshState !== null) { // ... } }
https://playground.react.dev/
https://react.dev/learn/react-compiler
























