React性能优化

342 阅读4分钟

一次优化经历

  发现性能问题

  需求: 在table每一行加checkbox来支持批量操作

  问题: 本地环境操作卡顿,生产环境也有点卡

   即使不做任何操作,也会因为轮询接口(三个接口)造成页面动画掉帧

image.png

性能检测

方法: 通过使用react DevTool Profiler录制10s,期间不做任何操作,但会自动轮询三个接口一次。

结果: 火焰图显示录制期间发生了34次rerender(重新渲染/更新), 且每次rerender基本会把整个灰度视图重新渲染一次,耗时长,平均每次rerender耗时120ms+

image.png

疑问1 : 录制期间接口返回的数据没发生变化,组件状态没有变化,不应该发生rerender

疑问2: 即使因为轮询三个接口,数据发生了变化,也不应该发生这么多次耗时长的rerender

问题分析

造成 React 组件 rerender的几种原因:

  1. 父组件rerender

  2. 组件内部因为useState或useReducer状态改变

  3. 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)去处理异步任务,在更新过程中,可以随时停止该任务并去执行优先级更高的任务。

image.png

FiberNode是React Virtual Dom中最主要的单位

Interface FiberNode{
    
    key: string;
    tag: number; // Fiber类型
    stateNode: HTMLElement; // 对应当前Fiber对应的真实DOM节点, React组件为null
    type: string | React.FCReact.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原生代码进行交互的目的,从而拿到渲染数据并对数据流与渲染归因进行展示

image.png

UseCallback/UseMemo

UseCallback通常用来缓存函数的引用

UseMemo通常用来缓存计算的数据,避免每次rerender都重新计算一遍

image.png

毫无意义的useCallback/useMemo会增加内存消耗,影响渲染速度;deps[]还会带来额外的心智负担

Reference

angularindepth.com/posts/1501/…

namansaxena-official.medium.com/react-virtu…