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的过程,否则需要更新。
具体可以这么显示实现:
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)props和state对象的,即只比较对象的第一层的属性及其值是不是相同。例如下面state对象变更为如下值:
state = { value: { foo: 'bar' } }因为state的value被赋予另一个对象,使nextState.value与this.props.value始终不等,导致浅比较通过不了。在实际项目中,这种嵌套的对象结果是很常见的,如果使用PureRenderMin或者PureComponent方式时起不到应有的效果。
虽然可以通过深比较方式来判断,但是深比较类似于深拷贝,递归操作,性能开销比较大。
为此,可以对组件尽可能的拆分,使组件的props和state对象数据达到扁平化,结合着使用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是浅比较组件的props和state来决定是否更新组件。
上面的列表组件中,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。