React组件的性能优化

react组件的渲染性能优化

  • 单个组件的性能优化
  • 多个组件的性能优化
  • 利用reselect提高数据选取的性能
  • 动画效果
  • 代码打包
  • ...

1、单个组件的性能优化

React利用Virtual DOM来提高渲染性能,虽然每一次页面更新都是对组件的重新渲染,但是并不是将之前渲染的内容全部抛弃重来,借助Virtual DOM, React能够计算出对DOM树的最少修改,这就是React默认情况下渲染都很迅捷的秘诀。不过,虽然Virtual DOM能够将每次DOM操作量减少到最小,计算和比较Virtual DOM依然是一个复杂的计算过程。如果能够在开始计算Virtual DOM之前就可以判断渲染结果不会有变化,那样可以干脆不要进行Virtual DOM计算和比较,速度就会更快

安装插件React  Perf ,可以发现react组件渲染的性能问题

redux-immutable-state-invariant提供了redux的中间件,能够让redux在每次派发动作之后做一个检查,若某个reducer违反了作为一个纯函数擅自修改了state,给出警告⚠️

render函数决定了“组件渲染出什么”,shouldComponentUpdate决定“什么时候不需要重新渲染”

 

2、多个组件的性能优化

装载阶段基本没有什么选择,当一个React组件第一次出现在DOM树中时,无论如何是要彻底渲染一次的,从这个React组件往下的所有子组件,都要经历一遍React组件的装载生命周期,因为这部分的工作没有什么可以省略的,所以没有多少性能优化的事情可做。至于卸载阶段,只有一个生命周期函数componentWillUnmount,这个函数做的事情只是清理componentDidMount添加的事件处理监听等收尾工作,做的事情比装载过程要少很多,所以也没有什么可优化的空间。所以值得关注的过程,就只剩下了更新过程。

 React的调和(Reconciliation)过程

在装载过程中,React通过render方法在内存中产生了一个树形的结构,树上每一个节点代表一个React组件或者原生的DOM元素,这个树形结构就是所谓的Virtual DOM。React根据这个VirtualDOM来渲染产生浏览器中的DOM树。在装载过程结束后,用户就可以对网页进行交互,用户操作引发了界面的更新,网页中需要更新界面,React依然通过render方法获得一个新的树形结构Virtual DOM,这时候当然不能完全和装载过程一样直接用Virtual DOM去产生DOM树,不然就和最原始的字符串模板一个做法。而且,在真实的应用中,大部分网页内容的更新都是局部的小改动,如果每个改动都是推倒重来,那样每次都重新完全生成DOM树,性能肯定不可接受。实际上,React在更新阶段很巧妙地对比原有的Virtual DOM和新生成的Virtual DOM,找出两者的不同之处,根据不同来修改DOM树,这样只需做最小的必要改动。React在更新中这个“找不同”的过程,就叫做Reconciliation(调和)。

 

按照计算机科学目前的算法研究结果,对比两个N个节点的树形结构的算法,时间复杂度是O(N3),打个比方,假如两个树形结构上各有100节点,那么找出这两个树形结构差别的操作,需要100× 100×100次操作,也就是一百万次当量的操作,假如有一千个节点,那么需要相当于进行相当于1000×1000× 1000次操作,这是一亿次的操作当量,这么巨大数量的操作在强调快速反应的网页中是不可想象的,所以React不可能采用这样的算法。React实际采用的算法需要的时间复杂度是O(N),因为对比两个树形怎么着都要对比两个树形上的节点,似乎也不可能有比O(N)时间复杂度更低的算法。

 Key的用法

React不会使用一个O(N2)时间复杂度的算法去找出前后两列子组件的差别,默认情况下,在React的眼里,确定每一个组件在组件序列中的唯一标识就是它的位置,所以它也完全不懂哪些子组件实际上并没有改变,为了让React更加“聪明”,就需要开发者提供一点帮助。如果在代码中明确地告诉React每个组件的唯一标识,就可以帮助React在处理这个问题时聪明很多,告诉React每个组件“身份证号”的途径就是key属性。

庆幸的是,React会提醒开发者不要忘记使用key,同类型子组件出现多个实例时如果没有key的话,React在运行时会给出警告

理解了key属性的作用,也就知道,在一列子组件中,每个子组件的key值必须唯一,不然就没有帮助React区分各个组件的身份,这并不是一个很难的问题,一般很容易给每个组件找到一个唯一的id。但是这个key值只是唯一还不足够,这个key值还需要是稳定不变的,试想,如果key值虽然能够在每个时刻都唯一,但是变来变去,那么就会误导React做出错误判断,甚至导致错误的渲染结果。如果通过数组来产生一组子组件,一个常见的错误就是将元素在数组中的下标值作为key,下面的代码是错误的例子:

这么做非常危险,因为,假如没有使用key的话React会在运行时输出一个错误提示,但是错误地使用key值React就不会给出错误提示了,因为React无法发现开发者的错误。用数组下标作为key,看起来key值是唯一的,但是却不是稳定不变的,随着todos数组值的不同,同样一个TodoItem实例在不同的更新过程中在数组中的下标完全可能不同,把下标当做key就让React彻底乱套了。需要注意,虽然key是一个prop,但是接受key的组件并不能读取到key的值,因为key和ref是React保留的两个特殊prop,并没有预期让组件直接访问。

三、用reselect提高数据获取性能

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

reselect库被用来创造“选择器”,所谓选择器,就是接受一个state作为参数的函数,这个选择器函数返回的数据就是我们某个mapStateToProps需要的结果。

两阶段选择过程

在前面的章节,我们已经强调过React组件的渲染函数应该是一个纯函数,Redux中的reducer函数也应该是一个纯函数,mapStateToProps函数也应该是纯函数,纯函数让问题清晰而且简化。不过,现在这个“选择器”函数可不是纯函数,它是一种有“记忆力”的函数,运行选择器函数会有副作用,副作用就是能够根据以往的运行“记忆”返回“记忆”中的结果。reselect认为一个选择器的工作可以分为两个部分,把一个计算过程分为两个步骤:

步骤1,从输入参数state抽取第一层结果,将这第一层结果和之前抽取的第一层结果做比较,如果发现完全相同,就没有必要进行第二部分运算了,选择器直接把之前第二部分的运算结果返回就可以了。注意,这一部分做的“比较”,就是JavaScript的===操作符比较,如果第一层结果是对象的话,只有是同一对象才会被认为是相同。

步骤2,根据第一层结果计算出选择器需要返回的最终结果。显然,每次选择器函数被调用时,步骤一都会被执行,但步骤一的结果被用来判断是否可以使用缓存的结果,所以并不是每次都会调用步骤二的运算。选择器就是利用这种缓存结果的方式,避免了没有必要的运算浪费。剩下的事情就是确定选择器步骤一和步骤二分别进行什么运算。原则很简单,步骤一运算因为每次选择器都要使用,所以一定要快,运算要非常简单,最好就是一个映射运算,通常就只是从state参数中得到某个字段的引用就足够,把剩下来的重活累活都交给步骤二去做。

 

在TodoList这个具体例子中,todos和filter的值直接决定应该显示什么样的待办事项,所以,很显然步骤一是获取todos和filter的值,步骤二就是根据这两个值进行计算

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

npm install --save reselect

在src/todos/selector.js文件中,选择器函数的代码如下:

reselect提供了创造选择器的createSelector函数,这是一个高阶函数,也就是接受函数为参数来产生一个新函数的函数。第一个参数是一个函数数组,每个元素代表了选择器步骤一需要做的映射计算,这里我们提供了两个函数getFilte和getTodos,对应代码如下:

上面说过,步骤一的运算要尽量简单快捷,所以往往一个Lambda表达式就足够。createSelector函数的第二个参数代表步骤二的计算过程,参数为第一个参数的输出结果,里面的逻辑和之前TodoList中的逻辑没有什么两样,只是这第二个函数不是每次都会被调用到。现在,我们可以在TodoList模块中改用新定义的选择器来获取待办事项数据了,代码如下:

Redux要求每个reducer不能修改state状态,如果要返回一个新的状态,就必须返回一个新的对象。如此一来,Redux Store状态树上某个节点如果没有改变,那么我们就有信心这个节点下数据没有改变,应用在reselect中,步骤一的运算就可以确定直接缓存运算结果。虽然reselect的createSelector创造的选择器并不是一个纯函数,但是createSelector接受的所有函数参数都是纯函数,虽然选择器有“记忆”这个副作用,但是只要输入参数state没有变化,产生的结果也就没有变化,表现得却类似于纯函数。只要Redux Store状态树上的filter和todos字段不变,无论怎样触发TodoList的渲染过程,都不会引发没有必要的遍历todos字段的运算,性能自然更快。虽然reselect库以re开头,但是逻辑上和React/Redux没有直接关系。实际上,在任何需要这种具有记忆的计算场合都可以使用reselect,不过,对于React和Redux组合的应用,reselect无疑能够提供绝佳的支持。

范式化状态树

所谓范式化,就是遵照关系型数据库的设计原则,减少冗余数据。如果读者做过关系型数据库(比如MySQL或者PostgreSQL)的表设计,就知道范式化的数据结构设计就是要让一份数据只存储一份,数据冗余造成的后果就是难以保证数据一致性。

与范式化相对,还存在“反范式化”的数据库设计,这在风生水起的NoSQL领域是一个惯常的设计风格。反范式化是利用数据冗余来换取读写效率,因为关系型数据库的强项虽然是保持一致,但是应用需要的数据形式往往是多个表join之后的结果,而join的过程往往耗时而且在分布式系统中难以应用。

 

假设我们给Todo应用做一个更大的改进,增加一个Type的概念,可以把某个TodoItem归为某一个Type,而且一个Type有特有的名称和颜色信息,在界面上,用户可以看到TodoItem显示为自己所属Type对应的颜色,TodoItem和Type当然是多对多的关系。用哪种方式设计状态树合适呢?更具体一点,如何设计代表TodoItem的状态树结构?如果使用反范式化的设计,那么状态树上的数据最好是能够不用计算拿来就能用,在Redux Store状态树的todos字段保存的是所有待办事项数据的数组,对于每个数组元素,反范式化的设计会是类似下面的对象:

在原有TodoItem对应状态中增加了type字段,包含name和color两个数据。当然,如果要达到数据扁平化的目的,应该是增加两个字段,分别为typeName和typeColor。这种状态设计的好处就是在渲染TodoItem组件时,从Redux Store上获得的状态可以直接使用name和color数据。但这也有缺点,当需要改变某种类型的名称和颜色时,不得不遍历所有TodoItem数据来完成改变。反范式化数据结构的特点就是读取容易,修改比较麻烦。如果使用范式化的数据结构设计,那么Redux Store上代表TodoItem的一条数据是类似下面的对象:

用一个typeId代表类型,然后在Redux Store上和todos平级的根节点位置创建一个types字段,内容是一个数组,每个数组元素代表一个类型,一个种类的数据是类似下面的对象:

当TodoItem组件要渲染内容时,从Redux Store状态树的todos字段下获取的数据是不够的,因为只有typeId。为了获得对应的种类名称和颜色,需要做一个类似关系型数据库的join操作,到状态树的types字段下去寻找对应typeId的种类数据。这个过程当然要花费一些时间,但是当要改变某个种类的名称或者颜色时,就异常地简单,只需要修改types中的一处数据就可以了。对比反范式方式和范式方式的优劣,不难看出范式方式更合理。因为虽然join数据需要花费计算时间,但是应用了reselect之后,大部分情况下都会命中缓存,实际上也就没有花费很多计算时间了。

总结

在本文中,我们了解了利用react-redux提供的shouldComponentUpdate实现来提高组件渲染功能的方法,一个要诀就是避免传递给其他组件的prop值是一个不同的对象,不然会造成无谓的重复渲染。通过了解React的Reconciliation过程,我们了解了React为什么能够在O(N)时间复杂度下完成VirutalDOM的比较,但是为了让React高效无误地完成工作,需要开发者做一些配合。首先,不能随意修改一个作为容器的HTML节点的类型。其次,对于动态数量的同类型子组件,一定要使用key这个prop。React的Reconciliation算法缺点是无法发现某个子树移动位置的情况,如果某个子树移动了位置,那React就会重新创建这个子树。当然,通常应用中不会出现这种情况。最后,我们学习了利用reselect库来实现高效的数据获取。因为reselect的缓存功能,开发者不用顾忌范式化的状态树会存在性能问题,Redux Store的状态树应该按照范式化原则来设计,减少数据冗余,这样利于保持数据一致。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值