React 性能调优

要优化性能,那首先需要对 react 渲染机制有一些深入的了解

背景知识

1.JS 变量类型

  • 基本类型:6 种基本数据类型, Undefined 、 Null 、 Boolean 、 Number 、 String 、 Symbol
  • 引用类型:统称为 Object 类型,细分为:Object 类型、 Array 类型、 Date 类型、 RegExp 类型、 Function 类型等

举个栗子:

1
2
3
4
5
6
7
let p1 = { name: '张三' };

let p2 = p1;

p2.name = '李四';

console.log(p1.name); // 李四

在引用类型里,声明一个 p1 的对象,把 p1 赋值给 p2 ,此时赋的其实是该对象的在堆中的地址,而不是堆中的数据,也就是两个变量指向的是同一个存储空间,后面 p2.name 改变后,也就影响到了 p1。虽然这样做可以节约内存,但当应用复杂后,就需要很小心的操作数据了,因为一不注意修改一个变量的值可能就影响到了另外一个变量

如果我们想要让他们不互相影响,就需要拷贝出一份一模一样的数据,拷贝又分浅拷贝与深拷贝,浅拷贝只会拷贝第一层的数据,深拷贝则会递归所有层级都拷贝一份,比较消耗性能

2.React 中的 Render

react 的组件渲染分为初始化渲染和更新渲染。
在初始化渲染的时候会调用根组件下的所有组件的render方法进行渲染,如下图(绿色表示已渲染,这一层是没有问题的):

但是当我们要更新某个子组件的时候,如下图的绿色组件(从根组件传递下来应用在绿色组件上的数据发生改变):

我们的理想状态是只调用关键路径上组件的render,如下图:

但是react的默认做法是调用所有组件的render,再对生成的虚拟DOM进行对比,如不变则不进行更新。而这里的render和虚拟DOM的对比明显是在浪费,如下图(黄色表示浪费的render和虚拟DOM对比)

这里有个问题,调用 render 是不是就是渲染 dom,答案是不会,生成的虚拟DOM进行对比

浪费的是 render和虚拟DOM的对比

在 React 中,每次 setState ,默认情况下, Render 后 Virtual DOM 会计算出前后两次虚拟 DOM 对象的区别,再去修改真实需要修改的 DOM 。由于 js 计算速度很快,而操作真实 DOM 相对比较慢,Virtual DOM 避免了没必要的真实 DOM 操作,所以从这个角度来说, React 性能很好

但随着应用复杂度的提升, DOM 树越来越复杂,大量的对比操作也会影响性能。比如一个 Table 组件,修改其中一行 Tr 组件的某一个字段, setState 后,其他所有行 Tr 组件也都会执行一次 render 函数,这其实是不必要的。我们可以通过 shouldComponentUpdate 函数决定是否更新组件。大部分时候我们是可以知道哪些组件是不会变的,根本就没必要去计算那一部分虚拟 DOM

Tips:

  • 拆分组件是有利于复用和组件优化的。
  • 生成虚拟DOM并进行比对发生在render()后,而不是render()前

解决方案

1.shouldComponentUpdate

react在每个组件生命周期更新的时候都会调用一个 shouldComponentUpdate(nextProps, nextState) 函数。它的职责就是返回true或false,true表示需要更新,false表示不需要,默认返回为true,即便你没有显示地定义 shouldComponentUpdate 函数。这就不难解释上面发生的资源浪费了。

为了进一步说明问题,我们再引用一张官网的图来解释,如下图( SCU表示 shouldComponentUpdate,绿色表示返回true(需要更新),红色表示返回false(不需要更新);vDOMEq表示虚拟DOM比对,绿色表示一致(不需要更新),红色表示发生改变(需要更新)):

根据渲染流程,首先会判断shouldComponentUpdate(SCU)是否需要更新。如果需要更新,则调用组件的render生成新的虚拟DOM,然后再与旧的虚拟DOM对比(vDOMEq),如果对比一致就不更新,如果对比不同,则根据最小粒度改变去更新DOM;如果SCU不需要更新,则直接保持不变,同时其子元素也保持不变。

  • C1根节点,绿色SCU (true),表示需要更新,然后vDOMEq红色,表示虚拟DOM不一致,需要更新
  • C2节点,红色SCU (false),表示不需要更新,所以C4,C5均不再进行检查
  • C3节点同C1,需要更新
  • C6节点,绿色SCU (true),表示需要更新,然后vDOMEq红色,表示虚拟DOM不一致,更新DOM
  • C7节点同C2
  • C8节点,绿色SCU (true),表示需要更新,然后vDOMEq绿色,表示虚拟DOM一致,不更新DOM

2.PureComponent

React15.3 中新加了一个类PureComponent,前身是 PureRenderMixin ,和 Component 基本一样,只不过会在 render 之前帮组件自动执行一次 shallowEqual(浅比较),来决定是否更新组件,浅比较类似于浅复制,只会比较第一层。使用 PureComponent 相当于省去了写 shouldComponentUpdate 函数,当组件更新时,如果组件的 props 和 state

  1. 引用和第一层数据都没发生改变, render 方法就不会触发,这是我们需要达到的效果
  2. 虽然第一层数据没变,但引用变了,就会造成虚拟 DOM 计算的浪费
  3. 第一层数据改变,但引用没变,会造成不渲染,所以需要很小心的操作数据

陷阱

设置缺省值:

1
<RadioGroup options={this.props.options || []} />

如果 this.props.options 的值为 false ,那么相当于每次 Render 的时候都给 options 传入了一个新的空数组引用,会引起渲染。正确的方式应该是用常量保存下来:

1
2
const DEFAULT_OPTIONS = []
<RadioGroup options={this.props.options || DEFAULT_OPTIONS} />

设置字面量样式:

1
<div style={{width: '100px'}}></div>

再比如给事件绑定函数:

1
2
3
4
5
6
7
<Button onClick={this.update.bind(this)} />

<Button
onClick={() => {
console.log("Click");
}}
/>

以上两种情况对组件来说,每次渲染的时候都是绑定的新的函数,所以也会造成子组件重新渲染

如下两种才是正确的方式:

1
2
3
4
5
6
7
8
9
10
class Counter extends React.Component {
constructor(props) {
super(props);
this.tick = this.tick.bind(this);
}
tick() {
...
}
...
}
1
2
3
4
5
6
class Counter extends React.Component {
tick = () => {
...
}
...
}