React Compiler - 解放在函数中编程时的性能焦虑(React Conf 2024)附 Next 在线演示
React Compiler
使用场景(为什么要使用?)
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
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;
}
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
npx react-compiler-healthcheck@latest
Successfully compiled 8 out of 9 components.
StrictMode usage not found.
Found no usage of incompatible libraries.
配置 eslint-plugin-react-compiler
pnpm install eslint-plugin-react-compiler
配置 .eslintrc.js
.eslintrc.jsmodule.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
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
pnpm install babel-plugin-react-compiler
babel.config.jsconst ReactCompilerConfig = {}; module.exports = function () { return { plugins: [ ['babel-plugin-react-compiler', ReactCompilerConfig], // must run first! // ... ], }; };
在 Next 中使用 React Compiler
pnpm install next@canary react@canary react-dom@canary babel-plugin-react-compiler
next.config.tsconst nextConfig = { experimental: { // reactCompiler: true, reactCompiler: { compilationMode: 'annotation', }, }, }; module.exports = nextConfig;
源码浅析
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/packages/babel-plugin-react-compiler/src/Entrypoint/Program.tsfunction 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.tsfunction 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
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 kshao-blog-前端知识记录!
评论