React组建的性能优化
性能优化的方法:
- 单个React组件的性能优化;
- 多个React组件的性能优化;
- 利用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首先检查两个树形的根节点的类型是否相同,根据相同或者不同有不同处理方式。
- 节点类型不同的情况
如果树形结构根节点类型不相同,直接认为原来的树形结构已经没有用,需要构建新的DOM树。原有的树形上React组件会经历“卸载”的生命周期。这种方式可能造成某种程度的浪费,但是为了避免较大的复杂度,React必须选择一个更简单跟快捷的算法。
也就是说,对于Virtual DOM树这是一个“更新”的过程,但是却可能引发这个树结构上某些组件的“装载”和“卸载”过程。
-
节点类型相同的情况
如果两个树形结构的根节点类型相同,React就认为原来的根节点只需要更新过程,不会将其卸载,也不会引发根节点的重新装载。
区分节点的类型:
- DOM元素类型,对应的是HTML直接支持的元素,如div、span、p;
React会保留节点对应的DOM元素,对树形根节点上的属性和内容做对比,只更新修改的部分。
-
React组件,利用React库定制的类型;
React根据新节点的props去更新原来根节点的组件实例,引发组件实例的更新过程,也就是按照顺序引发下列函数:
- shouldComponentUpdate
- componentWillReceiveProps
- componentWillUpdate
- render
- componentDidUpdate
- DOM元素类型,对应的是HTML直接支持的元素,如div、span、p;
在这个过程中,如果shouldComponentUpdate函数返回false,那么更新过程停止。为了保持最大的性能,每个人React组件必须要重视shouldConponentUpdate,如果发现没有必要重新渲染,那么直接返回false。
在处理完根节点的对比之后,React的算法会对根节点的每个子节点重复一样的动作,这时候每个子节点就成为它所覆盖部分的根节点,处理方式和它的父节点完全一样。
- 多个子组件的情况
当一个组件包含多个子组件的情况,React的处理方式也非常直接。
React选择了看起来很傻的办法,不是寻找两个序列的精确差别,而是直接挨个比较每个子组件。
2.2Key的用法
如果在代码中明确地告诉每个组建的唯一标识,就可以帮助React在处理这个问题时聪明很多,告诉每个组件“身份证号”的途径就是key属性。
在一列子组件中,每个子组件的key值必须唯一,不然就没有帮助React区分各个组件的身份。
如果key值不唯一,就会误导React做出错误的判断,甚至导致错误的渲染结果。
注意:虽然key是个prop,但是接受可以的组件并不能读取到key的值,因为key和ref是React保留的两个特殊prop,并没有预期让组件直接访问。
3.利用reselect提高数据选取的性能
3.1两阶段选择过程
reselect库的工作原理:只要相关状态没有改变,那就直接使用上一次的缓存结果。
reselect的计算过程分为两个步骤:
- 从输入参数state抽取第一层结果,将这一层结果和之前抽取的第一层结果做比较,如果发现完全相同,就没有必要进行第二部分运算,选择器直接把之前第二部分的运算结果返回就可以了。
- 根据第一层结果计算出选择器需要返回的最终结果。
使用reselect需要安装对应的npm包:
$ npm install --save reselect
Redux要求每个reducer不能修改state状态,如果要返回一个新的状态,就必须返回一个新的对象。
3.2范式化状态树
Redux的状态树应该设计的尽量扁平,使用reselect之后,状态树的设计应该尽量范式化(Normalized)。
范式化:就是遵照关系型数据库的设计原则,减少冗余数据。
范式化的数据结构就是要让一份数据只存储一份,数据冗余造成的后果就是难以保证数据一致性。
反范式化是利用数据冗余来换取读写效率。
反范式化数据结构的特点就是读取容易,修改比较麻烦。
对比反范式化和范式化方式的优劣,不能看出范式化更加合理。因为虽然join数据需要花费计算时间,但是应用reselect之后,大部分情况下都会命中缓存,实际也就是没有花费很多计算时间。