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
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
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
const 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
const 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;
}
解析
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,
);
}
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
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 kshao-blog-前端知识记录!
评论