查看: 121|回复: 0

从性能角度看react组件拆分的重要性

[复制链接]

该用户从未签到

发表于 2019-11-4 06:28:46 | 显示全部楼层 |阅读模式
React是一个UI层面的库,它采用假造DOM技能减少javascript与真正DOM的交互,提升了前端性能;采用单向数据流机制,父组件通过props将数据传递给子组件,这样让数据流向一目了然。一旦组件的props或则state发生改变,组件及其子组件都将重新re-render和vdom-diff,从而完成数据的流向交互。但是这种机制在某些情况下比如说数据量较大的情况下可能会存在一些性能问题。下面就来分析react的性能瓶颈,并用结合着react-addons-perf工具来说明react组件拆分的紧张性。
react性能瓶颈

要相识react的性能瓶颈,就需要知道react的渲染流程。它的渲染可以分为两个阶段:

  • 初始组件化
    该阶段会执行组件及其全部子组件的render方法,从而天生第一版的假造dom。
  • 组件更新渲染。
    组件的props大概state任意发生改变就会触发组件的更新渲染。默认情况下其也会执行该组件及其全部子组件的render方法获取新的假造dom。
我们说的性能瓶颈指的是组件更新阶段的情况。
react组件更新流程

通过上面分析可以知道组件更新具体过程如下:

  • 执行该组件及其全部子组件的render方法获取更新后的假造DOM,即re-render,纵然子组件无需更新。
  • 然后对新旧两份假造DOM举行diff来举行组件的更新
在这个过程中,可以通过组件的shouldComponentUpdate方法返回值来决定是否需要re-render。
react的整个更新渲染流程可以借用一张图来加以说明:

默认地,组件的shouldComponentUpdate返回true,即React默认会调用全部组件的render方法来天生新的假造DOM, 然后跟旧的假造DOM比力来决定组件最终是否需要更新。
react性能瓶颈

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

理想情况下,我们只希望关键路径上的组件举行更新,如下图:

但是,实际结果却是每个组件都完成re-render和virtual-DOM diff过程,虽然组件没有变更,这显着是一种浪费。如下图黄色部分表示浪费的re-render和virtual-DOM diff。

根据上面的分析,react的性能瓶颈主要表如今:
对于props和state没有变化的组件,react也要重新天生假造DOM及假造DOM的diff。
用shouldComponentUpdate来举行性能优化

针对react的性能瓶颈,我们可以通过react提供的shouldComponentUpdate方法来做点优化的事,可以有选择的举行组件更新,从而提升react的性能,具体如下:
shouldComponentUpdate需要判断当前属性和状态是否和上一次的相同,假如相同则不需要执行后续天生假造DOM及其diff的过程,否则需要更新。
具体可以这么显示实现:
  1. shouldComponentUpdate(nextProps, nextState){   return !isEqual(nextProps, this.props) || !isEqual(nextState, this.state)}
复制代码
其中,isEqual方法为判断两个对象是否相等(指的是其对象内容相等,而不是全等)。
通过显示覆盖shouldComponentUpdate方法来判断组件是否需要更新从而避免无用的更新,但是若为每个组件添加该方法会显得繁琐,幸亏react提供了官方的办理方案,具体做法:
方案对组件的shouldComponentUpdate举行了封装处理,实现对组件的当前属性和状态与上一次的举行浅对比,从而决定组件是否需要更新。
react在发展的差别阶段提供两套官方方案:

  • PureRenderMin
    一种是基于ES5的React.createClass创建的组件,配合该形式下的mixins方式来组合PureRenderMixin提供的shouldComponentUpdate方法。当然用ES6创建的组件也能利用该方案。
  1. import PureRenderMixin from 'react-addons-pure-render-mixin';class Example extends React.Component {  constructor(props) {    super(props);    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);}
复制代码

  • PureComponent
    该方案是在React 15.3.0版本发布的针对ES6而增加的一个组件基类:React.PureComponent。这显着对ES6方式创建的组件更加友好。
  1. import React, { PureComponent } from 'react'class Example extends PureComponent {  render() {    // ...  }}
复制代码
需要指出的是,不管是PureRenderMin还是PureComponent,他们内部的shouldComponentUpdate方法都是浅比力(shallowCompare)props和state对象的,即只比力对象的第一层的属性及其值是不是相同。例如下面state对象变更为如下值:
  1. state = {  value: { foo: 'bar' }}
复制代码
因为state的value被赋予另一个对象,使nextState.value与this.props.value始终不等,导致浅比力通过不了。在实际项目中,这种嵌套的对象结果是很常见的,假如利用PureRenderMin大概PureComponent方式时起不到应有的结果。
虽然可以通过深比力方式来判断,但是深比力类似于深拷贝,递归操作,性能开销比力大。
为此,可以对组件尽可能的拆分,使组件的props和state对象数据到达扁平化,结合着利用PureRenderMin大概PureComponent来判断组件是否更新,可以更好地提升react的性能,不需要开发职员过多关心。
组件拆分

组件拆分,在react中就是将组件尽可能的细分,便于复用和优化。拆分的具体原则:

  • 尽量使拆分后的组件更容易判断是否更新
这不太好理解,举个例子吧:假设我们定义一个父组件,其包罗了5000个子组件。有一个输入框输入操作,每次输入一个数字,对应的谁人子组件背景色变红。
  1.         
  2. [list]         
  3. [*]{el.name}    }   
  4. [/list]
复制代码
本例中,输入框组件和列表子组件有着显着的差别,一个是动态的,输入值比力频繁;一个是相对静态的,不管input怎么输入它就是5000项。输入框每输入一个数字都会导致全部组件re-render,这样就会造成列表子组件不必要的更新。
可以看出,上面列表组件的更新不容易被取消,因为输入组件和列表子组件的状态都置于父组件state中,二者共享;react不可能用shouldComponentUpdate的返回值来使组件一部分组件更新,另一部分不更新。 只有把他们拆分为差别的组件,每个组件只关心对应的props。拆分的列表组件只关心自己那部分属性,其他组件导致父组件的更新在列表组件中可以通过判断自己关心的属性值情况来决定是否更新,这样才能更好地举行组件优化。

  • 尽量使拆分组件的props和state数据扁平化
这主要是从组件优化的角度考虑的,假如组件不需过多关注性能,可以忽略。
拆分组件之所以扁平化,是因为React提供的优化方案PureRenderMin大概PureComponent是浅比力组件的props和state来决定是否更新组件。
上面的列表组件中,this.state.items存放的是对象数组,为了更好的判断每项列表是否需要更新,可以将每个li列表项单独拆分为一个列表项组件,每个列表项相关的props就是items数组中的每个对象,这种扁平化数据很容易判断是否数据发生变化。
组件拆分的一个例子

为了这篇文章专门写了一个有关添加展示Todo列表的事例库。克隆代码到本地可以在本地运行结果。
该事例库是一个有着5000项的Todo列表,可以删除和新增Todo项。该事例展示了组件拆分前和拆分后的体验对比情况,可以发现有性能显着的提升。
下面我们结合react的性能检测工具react-addons-perf来说明组件拆分的情况。
拆分前的组件ToDOSBeforeDivision的render部分内容如下:
  1. add todo{    this.state.items.map(el=>{        return (          )      })}
复制代码
组件拆分前,输入框输入字符、增加todo大概删除todo项可以看出有显着的卡顿征象,如下图所示:

为了弄清楚是什么原因导致卡顿征象,我们利用chrome的devTool来定位,具体的做法是利用最新版的chrome浏览器的Performance选项来完成。先点击该选项中的record按钮开始记载,这时我们在组件输入框输入一个字符,然后点击stop来停止记载,我们会看到组件从输入开始到竣事这段时间内的一个性能profile。

从图可以看出我们在输入单个字符时,输入框的input事件逻辑险些占据整个响应时间,具体的处理逻辑主要是react层面的batchedUpdates方法批量更新列表组件,而不是用户自定义的逻辑。
那么,批量更新为啥占据这么多时间呢,为了搞清楚原因,我们借助基于react-addons-perf的chrome插件chrome-react-perf,它以chrome插件的形式输出分析的结果。
利用该插件需要留意一点的是:
chrome-react-perf插件的利用需要在项目中引入react-addons-perf模块,并必须将其对象挂载到window全局对象的Perf属性上,否则不能利用。
在devTool工具中选择Perf选项试图,点击start按钮后其变成stop按钮,在组件输入框中输入一个字符,然后点击Perf试图中的stop按钮,就会得出对应的性能试图。

上图提供的4个视图中,Print Wasted对分析性能最有帮组,它表示组件没有变化但是参与了更新过程,即浪费了re-render和vdom-diff这一过程,是毫无意义的过程。从图可以看出:TodosBeforeDivision和TodoItem组件分别浪费了167.88ms、144.47ms,这完全可以通过拆分组件避免的开销,这是react性能优化重点。
为此我们需要对TodosBeforeDivision组件举行拆分,拆分为一个带有input和button的动态组件AddTodoForm和一个相对静态的组件TodoList。二者分别继承React.PureComponent可以避免不必要的组件更新。
  1. export default class AddTodoForm extends React.PureComponent{...render(){    return (                      add todo          )  }...}
复制代码
其中TodoList组件还需要为每项Todo任务拆分为一个组件TodoItem,这样每个TodoItem组件的props对象为扁平化的数据,可以充分利用React.PureComponent来举行对象浅比力从而更好地决定组件是否要更新,这样避免了新增大概删除一个TodoItem项时,其他TodoItem组件不必更新。
  1. export default class TodoList extends React.PureComponent{  ...    render(){    return (              {this.props.initailItems.map(el=>{          return         })}      
  2.     )  } ...}export default class TodoItem extends React.PureComponent{  ...  render(){    return (              x        {this.props.item.text}        {this.props.tags.map((tag) => {          return  {tag};        })}      
  3.     )  }...}
复制代码
这样拆分后的组件,在用上面的性能检测工具查看对应的结果:


从上面的截图可以看出,拆分后的组件性能有了上百倍的提升,虽然其中还包罗一些其他优化,例如不将function在组件属性位置绑定this以及常量对象props缓存起来等避免每次re-render时重新天生新的function和新的对象props。
总的来说,对react组件举行拆分对react性能的提升是非常紧张的,这也是react性能优化的一个方向。
参考文献


相关技术服务需求,请联系管理员和客服QQ:2753533861或QQ:619920289
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

帖子推荐:
客服咨询

QQ:2753533861

服务时间 9:00-22:00

快速回复 返回顶部 返回列表