一次优化经历
发现性能问题
需求: 在table每一行加checkbox来支持批量操作
问题: 本地环境操作卡顿,生产环境也有点卡
即使不做任何操作,也会因为轮询接口(三个接口)造成页面动画掉帧
性能检测
方法: 通过使用react DevTool Profiler录制10s,期间不做任何操作,但会自动轮询三个接口一次。
结果: 火焰图显示录制期间发生了34次rerender(重新渲染/更新), 且每次rerender基本会把整个灰度视图重新渲染一次,耗时长,平均每次rerender耗时120ms+
疑问1 : 录制期间接口返回的数据没发生变化,组件状态没有变化,不应该发生rerender
疑问2: 即使因为轮询三个接口,数据发生了变化,也不应该发生这么多次耗时长的rerender
问题分析
造成 React 组件 rerender的几种原因:
-
父组件rerender
-
组件内部因为useState或useReducer状态改变
-
Context状态改变
疑问1分析: 即使数据没有变化,也会因为调用接口生成一份新的data引用,导致组件状态变化
疑问2分析: 通过profiler观察每次rerender的原因后,发现调完一个接口后表格组件渲染了4次(传参改变1次,内部3次),因为表格组件要渲染批量数据,层级多,dom数量大,所以每次rerender耗时长。表格组件内部不好改,但可以减少表格组件的渲染次数
解决办法
针对问题1: 将data做一次缓存,每次轮询时将新data和旧data做对比,没有变化则使用旧的data引用
针对问题2: 对表格组件的传参使用useCallback/useMemo来保留引用,对表格组件使用React.memo
效果:
10秒内rerender 8次,每次平均花费20ms+,即使在本地环境,列表也不卡了
React组件的一次更新
Render Phase 和 Commit Phase
FiberNode 和 FiberTree
- React 16之前,setState到render的过程是同步的,遍历虚拟Dom tree的时候是以递归的形式,中途无法中断,这个过程中js一直占用主线程,如果占用时间超过16.6ms,动画就会掉帧,造成卡顿
- React 16之后,为了解决同步递归渲染导致的卡顿问题,引入了Fiber架构,用每一帧空闲的时间(requestIdleCallback)去处理异步任务,在更新过程中,可以随时停止该任务并去执行优先级更高的任务。
FiberNode是React Virtual Dom中最主要的单位
Interface FiberNode{
key: string;
tag: number; // Fiber类型
stateNode: HTMLElement; // 对应当前Fiber对应的真实DOM节点, React组件为null
type: string | React.FC | React.Component; // dom为对应元素类型,组件指向组件对应的类或者函数
memoizedProps: Record<string, any>; // 记录上一次更新完毕后的props
memoizedState: Record<string, any>; // 类组件保存state信息,函数组件保存hooks信息,dom元素为null
// 用于连接其他FiberNode形成Fiber Tree
child: FiberNode; // 子节点
return: FiberNode; // 父节点
sibling: FiberNode; // 同胞节点
// react-dev-tool会根据这些时间统计来评估性能
actualDuration: number, // 本次更新过程, 本节点以及子树所消耗的总时间
actualStartTime: number, // 标记本fiber节点开始构建的时间
selfBaseDuration: number, // 用于最近一次生成本fiber节点所消耗的时间
treeBaseDuration: number, // 生成子树所消耗的时间的总和
// 指向该fiber在一次更新时指向的fiber
alternate: FiberNode;
......
}
// FiberNode类型
export const FunctionComponent = 0; // 对应函数组件
export const ClassComponent = 1; // 对应的类组件
export const IndeterminateComponent = 2; // 初始化的时候不知道是函数组件还是类组件
export const HostRoot = 3; // Root Fiber 可以理解为跟元素 , 通过reactDom.render()产生的根元素
export const HostPortal = 4; // 对应 ReactDOM.createPortal 产生的 Portal
export const HostComponent = 5; // dom 元素 比如 <div>
export const HostText = 6; // 文本节点
export const Fragment = 7; // 对应 <React.Fragment>
export const Mode = 8; // 对应 <React.StrictMode>
export const ContextConsumer = 9; // 对应 <Context.Consumer>
export const ContextProvider = 10; // 对应 <Context.Provider>
export const ForwardRef = 11; // 对应 React.ForwardRef
export const Profiler = 12; // 对应 <Profiler/ >
export const SuspenseComponent = 13; // 对应 <Suspense>
export const MemoComponent = 14; // 对应 React.memo 返回的组件
获取根节点FiberNode: document.getElementById("root")._reactRootContainer._internalRoot.current
Current Tree 和 WIP Tree (双缓冲树)
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
作用:
- 复杂canvas绘制避免白屏闪烁
- 防止更新状态丢失
React性能检测工具
React DevTools
React官方插件,挂载全局对象window.__REACT_DEVTOOLS_GLOBAL_HOOK__
在ReactDom中,会判断有没有这个对象,有的话就注入所需要的信息
__REACT_DEVTOOLS_GLOBAL_HOOK__.inject(internal)
PerfGo
该插件也是通过覆写__REACT_DEVTOOLS_GLOBAL_HOOK__中与渲染相关的方法,达到与react原生代码进行交互的目的,从而拿到渲染数据并对数据流与渲染归因进行展示
UseCallback/UseMemo
UseCallback通常用来缓存函数的引用
UseMemo通常用来缓存计算的数据,避免每次rerender都重新计算一遍
毫无意义的useCallback/useMemo会增加内存消耗,影响渲染速度;deps[]还会带来额外的心智负担