React组建的性能优化

性能优化的方法:

  1. 单个React组件的性能优化;
  2. 多个React组件的性能优化;
  3. 利用reselect提高数据选取的性能;

重点关注的是,React组件的渲染性能优化。

1.单个React组件的性能优化

React利用Virtual DOM来提高渲染性能,虽然每次页面更新都是对组件的重新渲染,但并不是将之前渲染的内容全部抛弃重来,借助Virtual DOM,React能够计算出DOM树最少的修改,这就是React在默认情况下都渲染很迅速的秘籍。

1.1发现浪费的渲染时间

发现浪费的渲染时间,需要在Chrome浏览器中安装React Perf扩展。

注意:这里的浪费是计算Virtual DOM的浪费,而不是访问DOM树的浪费。

1.2性能优化的时机

“过早的优化是万恶之源” - 高德纳 《计算机编程艺术》

表面上的意思是,除非性能出了问题,不然就不要花时间去优化性能问题。

真正上的意思是,“我们应该忘记忽略很小的性能优化,可以说97%的情况下,过早的优化是万恶之源,而我们应该关心对性能影响最关键的那另外的3%的代码” - 高德纳。

高德纳认为,不要将性能优化的经历浪费在对整体性能提高不大的代码,而是对性能有关键的影响的部分,优化并不嫌早。

“过早的优化”:没有任何量化证据的情况下,开发者对性能优化的猜测,并没有可测量的性能指标,就完全不知道当前的性能瓶颈在何处,完成优化之后,也无法知道性能优化是否达到了预期的结果。

1.3React-Redux的shouldComponentUpdate实现

shouldComponentUpdate的默认实现是一种兜底的保险方法。但是要达到更好的性能,有必要定义shouldComponentUpdate函数。

React-Redux通过提供更好的shouldComponentUpdate的实现方式:

在对比prop和上次渲染所用prop方面上看,依然使用的是“浅层比较”(shallow compare)。可以简单的理解为,JS 的“===”操作符。

想让React-Redux认为前后的对象prop是相同的,就必须保证prop指向的JS对象是一致的。

2.多个React组件的性能优化

和单个React组建的生命周期一样,多个React组件也要考虑三个阶段:装载更新卸载

装载阶段:

React组件往下的所有子组件,都需要彻底渲染一次,都要经历一遍React组件的装载生命周期。因此,没有多少性能优化的东西。

卸载阶段:

只有一个生命周期函数componentWillUnmount,只是清理componentDidMount,做的事情比装载阶段还少,也没有什么可优化的空间。

值得关心的过程,只剩下更新阶段。

2.1React的调和(Reconciliation)过程

在装载的过程中,React通过render方法在内存中产生了一个树形结构,树上每一个节点代表一个React 组件或者原生DOM元素,这个树形结构就是所谓的Virtual DOM。React根据这个Virtual DOM 来渲染浏览器的DOM树。

Reconciliation(调和)

在更新阶段巧妙地原有的Virtual DOM和新生成的Virtual DOM,找出两者的不同之处。根据不同来修改DOM树,更新中这个“找不同”的过程就叫做Reconciliation(调和)。

Reconciliation算法:

当React要对比两个Virtual DOM的树形结构时,从根节点开始递归往下对比,在这个树形结构中,每个节点都可以看作当前节点以下部分子树的根节点。所以这个对比算法可以从Virtual DOM上任何一个节点开始执行。

React首先检查两个树形的根节点的类型是否相同,根据相同或者不同有不同处理方式。

  1. 节点类型不同的情况

    如果树形结构根节点类型不相同,直接认为原来的树形结构已经没有用,需要构建新的DOM树。原有的树形上React组件会经历“卸载”的生命周期。这种方式可能造成某种程度的浪费,但是为了避免较大的复杂度,React必须选择一个更简单跟快捷的算法。

    也就是说,对于Virtual DOM树这是一个“更新”的过程,但是却可能引发这个树结构上某些组件的“装载”和“卸载”过程。

  2. 节点类型相同的情况

    如果两个树形结构的根节点类型相同,React就认为原来的根节点只需要更新过程,不会将其卸载,也不会引发根节点的重新装载。

    区分节点的类型:

    1. DOM元素类型,对应的是HTML直接支持的元素,如div、span、p;

      React会保留节点对应的DOM元素,对树形根节点上的属性和内容做对比,只更新修改的部分。

    2. React组件,利用React库定制的类型;

      React根据新节点的props去更新原来根节点的组件实例,引发组件实例的更新过程,也就是按照顺序引发下列函数:

      • shouldComponentUpdate
      • componentWillReceiveProps
      • componentWillUpdate
      • render
      • componentDidUpdate
  在这个过程中,如果shouldComponentUpdate函数返回false,那么更新过程停止。为了保持最大的性能,每个人React组件必须要重视shouldConponentUpdate,如果发现没有必要重新渲染,那么直接返回false。

  在处理完根节点的对比之后,React的算法会对根节点的每个子节点重复一样的动作,这时候每个子节点就成为它所覆盖部分的根节点,处理方式和它的父节点完全一样。
  1. 多个子组件的情况

    当一个组件包含多个子组件的情况,React的处理方式也非常直接。

    React选择了看起来很傻的办法,不是寻找两个序列的精确差别,而是直接挨个比较每个子组件。

2.2Key的用法

如果在代码中明确地告诉每个组建的唯一标识,就可以帮助React在处理这个问题时聪明很多,告诉每个组件“身份证号”的途径就是key属性。

在一列子组件中,每个子组件的key值必须唯一,不然就没有帮助React区分各个组件的身份。

如果key值不唯一,就会误导React做出错误的判断,甚至导致错误的渲染结果。

注意:虽然key是个prop,但是接受可以的组件并不能读取到key的值,因为key和ref是React保留的两个特殊prop,并没有预期让组件直接访问。

3.利用reselect提高数据选取的性能

3.1两阶段选择过程

reselect库的工作原理:只要相关状态没有改变,那就直接使用上一次的缓存结果。

reselect的计算过程分为两个步骤:

  1. 从输入参数state抽取第一层结果,将这一层结果和之前抽取的第一层结果做比较,如果发现完全相同,就没有必要进行第二部分运算,选择器直接把之前第二部分的运算结果返回就可以了。
  2. 根据第一层结果计算出选择器需要返回的最终结果。

使用reselect需要安装对应的npm包:

$ npm install --save reselect

Redux要求每个reducer不能修改state状态,如果要返回一个新的状态,就必须返回一个新的对象。

3.2范式化状态树

Redux的状态树应该设计的尽量扁平,使用reselect之后,状态树的设计应该尽量范式化(Normalized)。

范式化:就是遵照关系型数据库的设计原则,减少冗余数据。

范式化的数据结构就是要让一份数据只存储一份,数据冗余造成的后果就是难以保证数据一致性。

反范式化是利用数据冗余来换取读写效率。

反范式化数据结构的特点就是读取容易,修改比较麻烦。

对比反范式化和范式化方式的优劣,不能看出范式化更加合理。因为虽然join数据需要花费计算时间,但是应用reselect之后,大部分情况下都会命中缓存,实际也就是没有花费很多计算时间。


BleakNight
4 声望1 粉丝