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

React是一个UI层面的库,它采用虚拟DOM技术减小Javascript与真正DOM的交互,提高了前端性能;采用单向数据流机制,父组件经过props将数据传递给子组件,这样让数据流向一目了然。一旦组件的props或则state发生改变,组件及其子组件都将从新re-render和vdom-diff,从而完成数据的流向交互。可是这种机制在某些状况下好比说数据量较大的状况下可能会存在一些性能问题。下面就来分析react的性能瓶颈,并用结合着react-addons-perf工具来讲明react组件拆分的重要性。javascript

react性能瓶颈

要了解react的性能瓶颈,就须要知道react的渲染流程。它的渲染能够分为两个阶段:前端

  • 初始组件化
    该阶段会执行组件及其全部子组件的render方法,从而生成初版的虚拟dom。java

  • 组件更新渲染
    组件的props或者state任意发生改变就会触发组件的更新渲染。默认状况下其也会执行该组件及其全部子组件的render方法获取新的虚拟dom。react

咱们说的性能瓶颈指的是组件更新阶段的状况。linux

react组件更新流程

经过上面分析能够知道组件更新具体过程以下:git

  • 执行该组件及其全部子组件的render方法获取更新后的虚拟DOM,即re-render,即便子组件无需更新。github

  • 而后对新旧两份虚拟DOM进行diff来进行组件的更新chrome

在这个过程当中,能够经过组件的shouldComponentUpdate方法返回值来决定是否须要re-render。segmentfault

react的整个更新渲染流程能够借用一张图来加以说明:数组

默认地,组件的shouldComponentUpdate返回true,即React默认会调用全部组件的render方法来生成新的虚拟DOM, 而后跟旧的虚拟DOM比较来决定组件最终是否须要更新。

react性能瓶颈

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

理想状况下,咱们只但愿关键路径上的组件进行更新,以下图:

可是,实际效果倒是每一个组件都完成re-rendervirtual-DOM diff过程,虽然组件没有变动,这明显是一种浪费。以下图黄色部分表示浪费的re-render和virtual-DOM diff。

根据上面的分析,react的性能瓶颈主要表如今:

对于propsstate没有变化的组件,react也要从新生成虚拟DOM及虚拟DOM的diff。

shouldComponentUpdate来进行性能优化

针对react的性能瓶颈,咱们能够经过react提供的shouldComponentUpdate方法来作点优化的事,能够有选择的进行组件更新,从而提高react的性能,具体以下:

shouldComponentUpdate须要判断当前属性和状态是否和上一次的相同,若是相同则不须要执行后续生成虚拟DOM及其diff的过程,不然须要更新。

具体能够这么显示实现:

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建立的组件也能使用该方案。
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方式建立的组件更加友好。
import React, { PureComponent } from 'react'
class Example extends PureComponent {
  render() {
    // ...
  }
}

须要指出的是,不论是PureRenderMin仍是PureComponent,他们内部的shouldComponentUpdate方法都是浅比较(shallowCompare)propsstate对象的,即只比较对象的第一层的属性及其值是否是相同。例以下面state对象变动为以下值:

state = {
  value: { foo: 'bar' }
}

由于state的value被赋予另外一个对象,使nextState.valuethis.props.value始终不等,致使浅比较经过不了。在实际项目中,这种嵌套的对象结果是很常见的,若是使用PureRenderMin或者PureComponent方式时起不到应有的效果。

虽然能够经过深比较方式来判断,可是深比较相似于深拷贝,递归操做,性能开销比较大。

为此,能够对组件尽量的拆分,使组件的propsstate对象数据达到扁平化,结合着使用PureRenderMin或者PureComponent来判断组件是否更新,能够更好地提高react的性能,不须要开发人员过多关心。

组件拆分

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

  • 尽可能使拆分后的组件更容易判断是否更新

这不太好理解,举个例子吧:假设咱们定义一个父组件,其包含了5000个子组件。有一个输入框输入操做,每次输入一个数字,对应的那个子组件背景色变红。

<div>
    <input value={this.state.inputText} onChange={this.inputChanged}/>
    <ul
    {
     this.state.items.map(el=>
         <li key={el.id} style={{background: index===this.state.inputText? 'red' : ''}}>{el.name}</li>
    }
    </ul>
</div>

本例中,输入框组件和列表子组件有着明显的不一样,一个是动态的,输入值比较频繁;一个是相对静态的,无论input怎么输入它就是5000项。输入框每输入一个数字都会致使全部组件re-render,这样就会形成列表子组件没必要要的更新。

能够看出,上面列表组件的更新不容易被取消,由于输入组件和列表子组件的状态都置于父组件state中,两者共享;react不可能用shouldComponentUpdate的返回值来使组件一部分组件更新,另外一部分不更新。 只有把他们拆分为不一样的组件,每一个组件只关心对应的props。拆分的列表组件只关心本身那部分属性,其余组件致使父组件的更新在列表组件中能够经过判断本身关心的属性值状况来决定是否更新,这样才能更好地进行组件优化。

  • 尽可能使拆分组件的props和state数据扁平化

这主要是从组件优化的角度考虑的,若是组件不需过多关注性能,能够忽略。

拆分组件之因此扁平化,是由于React提供的优化方案PureRenderMin或者PureComponent是浅比较组件的propsstate来决定是否更新组件。

上面的列表组件中,this.state.items存放的是对象数组,为了更好的判断每项列表是否须要更新,能够将每一个li列表项单独拆分为一个列表项组件,每一个列表项相关的props就是items数组中的每一个对象,这种扁平化数据很容易判断是否数据发生变化。

组件拆分的一个例子

为了这篇文章专门写了一个有关添加展现Todo列表的事例库。克隆代码到本地能够在本地运行效果。

该事例库是一个有着5000项的Todo列表,能够删除和新增Todo项。该事例展现了组件拆分前和拆分后的体验对比状况,能够发现有性能明显的提高。

下面咱们结合react的性能检测工具react-addons-perf来讲明组件拆分的状况。

拆分前的组件TodosBeforeDivision的render部份内容以下:

<input value={this.state.value} onChange={this.inputChange.bind(this)}/>
<button onClick={this.addTodo.bind(this)}>add todo</button>
{
    this.state.items.map(el=>{
        return (
          <TodoItem key={el.id} item={el} 
            tags={['important', 'starred']}
            deleteItem={this.deleteItem.bind(this, el.id)}/>)
      })
}

组件拆分前,输入框输入字符、增长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这一过程,是毫无心义的过程。从图能够看出:TodosBeforeDivisionTodoItem组件分别浪费了167.88ms、144.47ms,这彻底能够经过拆分组件避免的开销,这是react性能优化重点。

为此咱们须要对TodosBeforeDivision组件进行拆分,拆分为一个带有input和button的动态组件AddTodoForm和一个相对静态的组件TodoList。两者分别继承React.PureComponent能够避免没必要要的组件更新。

export default class AddTodoForm extends React.PureComponent{
...
render(){
    return (
      <form>
        <input value={this.state.value} onChange={this.inputChange}/>
        <button onClick={this.addTodo}>add todo</button>
      </form>
    )
  }
...
}

其中TodoList组件还须要为每项Todo任务拆分为一个组件TodoItem,这样每一个TodoItem组件的props对象为扁平化的数据,能够充分利用React.PureComponent来进行对象浅比较从而更好地决定组件是否要更新,这样避免了新增或者删除一个TodoItem项时,其余TodoItem组件没必要更新。

export default class TodoList extends React.PureComponent{
  ...  
  render(){
    return (
      <div>
        {this.props.initailItems.map(el=>{
          return <TodoItem key={el.id} item={el} tags={this.props.tags} deleteItem={this.props.deleteItem}/>
        })}
      </div>
    )
  }
 ...
}

export default class TodoItem extends React.PureComponent{
  ...
  render(){
    return (
      <div>
        <button style={{width: 30}} onClick={this.deleteItem}>x</button>
        <span>{this.props.item.text}</span>
        {this.props.tags.map((tag) => {
          return <span key={tag} className="tag"> {tag}</span>;
        })}
      </div>
    )
  }
...
}

这样拆分后的组件,在用上面的性能检测工具查看对应的效果:

图片描述

图片描述

从上面的截图能够看出,拆分后的组件性能有了上百倍的提高,虽然其中还包含一些其余优化,例如不将function在组件属性位置绑定this以及常量对象props缓存起来等避免每次re-render时从新生成新的function和新的对象props。

总的来讲,对react组件进行拆分对react性能的提高是很是重要的,这也是react性能优化的一个方向。

参考文献

相关文章
相关标签/搜索