一、引子
react的渲染机制是非常独特的,有别于 Vue 框架的渲染次数的优化计算。React 很久以来就有PureComponent、shouldUpdate。Function component 又有了memo、useMemo、useCallback 这样的函数工具,让它成为有一定深度的前端框架。
怎么使用 useMemo 和 useCallback 是我们值得思考的点。
二、代码范式
首先,假设大家对 React 都有一个基础的入门水平,所以本文不再赘述“useMemo 和 useCallback ”基本用法。《React 官网》、《Why React Re-Renders》和《Understanding useMemo and useCallback》很好讲解了基本用法。
2.1 Memo 缓存组件
引起重渲染的最常见的情况是,组件的 props。memo包裹着函数组件,针对props 参数的浅对比。
子组件的渲染有些情况,子组件控制不住,它受到父组件的参数影响。
function _Boxes({ boxes }: {
boxes: {
flex: number;
background: string;
}[]
}) {
return (
<div className="boxes-wrapper">
{boxes.map((boxStyles, index) => (
<div className="box" style={boxStyles} key={index} />
))}
</div>
);
}
const Boxes = memo(_Boxes);
2.2 父组件层面,对象使用 useMemo
作为 Box 的父组件,boxes 的传参是一个对象,每次改变,它会生成一个全新的对象。这里要对 boxes 对象使用缓存(useMemo)。这样,age 的改动将不会影响到 Box 的重渲染。
function App() {
const [age, setAge] = React.useState(0);
const [boxWidth, setBoxWidth] = React.useState(1);
const id = React.useId();
// Age 属性的变更不会影响 boxes 属性变化
const boxes = useMemo(() => {
return [
{ flex: boxWidth, background: 'hsl(345deg 100% 50%)' },
{ flex: 3, background: 'hsl(260deg 100% 40%)' },
{ flex: 1, background: 'hsl(50deg 100% 60%)' },
];
}, [boxWidth]);
return (
<>
<Boxes boxes={boxes} />
<section>
<button onClick={() => {
setAge(age + 1)
}}>
Increment age
</button>
<p>Hello! You are {age}.</p>
</section>
<section>
<label htmlFor={`${id}-box-width`}>
First box width:
</label>
<input
id={`${id}-box-width`}
type="range"
min={1}
max={5}
step={0.01}
value={boxWidth}
onChange={(event) => {
setBoxWidth(Number(event.target.value));
}}
/>
</section>
</>
);
}
2.3 context provider 最好用 useMemo
同理,参考《How To useContext With useReducer》,context provider 的 value 参数是一个很容易被遗忘的点,provider 可能会传入一个对象,利用useMemo 或者 useCallback 来保护 App 组件不会做出过多重渲染。
const Main = () => {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// 利用useMemo 或者 useCallback 来保护 Context 不会做出过多重渲染
const contextValue = useMemo(() => {
return { state, dispatch };
}, [state, dispatch]);
return (
<MyContext.Provider value={contextValue}>
<App />
</MyContext.Provider>
)
}
《useContext + useReducer re-renders》
2.4 其他
还有一种特殊情况是:不规范、分批的 Context 调用导致了页面的重新渲染。针对一些老旧项目,以前的业务逻辑导致 Context 的调用混乱,已经不是前面几种方法能够解决的。
解决方法:改动代码,把多次 Context 调用整合为一次。
三、补救防范
3.1 断点查看调用堆栈
利用 Chrome 原生调试工具打断点,看每一行代码的堆栈信息。这种方式最为原始但是它往往“行之有效”。
3.2 devtool 查看渲染次数和渲染堆栈
React Devtool 的 Profiler 能协助我们排查 React 渲染次数和渲染堆栈。
- 点击 Profiler 的记录圆圈
- 刷新页面或者做其他操作
- 停止记录
- 参考快照记录
同时,我们还能够通过“highlight updates when components render”来可视化整个渲染过程。
3.3 渲染打印工具
ahooks的useWhyDidYouUpdate
该函数能够帮助开发者排查是哪个属性改变导致了组件的 rerender,但是更多集中在 props、state,开发者需要主动去缩小范围,它起到辅助打印的工作,如果是 Context 或者更外侧的数据变动,效果不见得达到效果。
Welldone Software 的 why-did-you-render
该工具能全局打印 rerender 日志,但是在复杂项目当中,它的打印较为混乱,不一定能够很好的发现问题。
四、总结
React 的渲染优化有非常多篇博客已经聊过,但是还是“No Silver Bullet”,没有最佳方案。特别在一些数据流非常复杂的前端工程项目当中。
React 的前端项目能够划分为:聪明组件和懒惰组件。聪明组件负责的内容是页面逻辑的数据流向,懒惰组件负责的是样式的渲染,数据流越是清晰,代码可维护性越强。
以下是我个人的代码编写建议:
- 不要把所有数据都往 Context 里面放,只有极其核心,多个页面都复用情况。
- 简单的页面,尽量以聪明组件和懒惰组件的方式来处理。
- 如果已经出现数据流混乱的情况下,合理使用 memo,或者状态管理工具(例如zustand、jotai等)让代码数据流尽量走向清晰,初始化主路径尽量批量渲染好。
虽然有很多分析工具,但是它们更多是辅助,最重要还是要通过人工分析。重点要分析出渲染次数过多的代码,做出针对性地处理。