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-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性能优化的一个方向。

参考文献

从性能角度看react组件拆分的重要性的更多相关文章

  1. 从源码的角度看 React JS 中批量更新 State 的策略(下)

    这篇文章我们继续从源码的角度学习 React JS 中的批量更新 State 的策略,供我们继续深入学习研究 React 之用. 前置文章列表 深入理解 React JS 中的 setState 从源 ...

  2. 从源码的角度看 React JS 中批量更新 State 的策略(上)

    在之前的文章「深入理解 React JS 中的 setState」与 「从源码的角度再看 React JS 中的 setState」 中,我们分别看到了 React JS 中 setState 的异步 ...

  3. React 组件性能优化探索实践

    转自:http://www.tuicool.com/articles/Ar6Zruq React本身就非常关注性能,其提供的虚拟DOM搭配上Diff算法,实现对DOM操作最小粒度的改变也是非常的高效. ...

  4. React 组件性能优化

    React组件性能优化 前言 众所周知,浏览器的重绘和重排版(reflows & repaints)(DOM操作都会引起)才是导致网页性能问题的关键.而React虚拟DOM的目的就是为了减少浏 ...

  5. 从工程化角度讨论如何快速构建可靠React组件

    前言 React 的开发也已经有2年时间了,先从QQ的家校群,转成做互动直播,主要是花样直播这一块.切换过来的时候,业务非常繁忙,接手过来的业务比较凌乱,也没有任何组件复用可言. 为了提高开发效率,去 ...

  6. React组件性能优化总结

    性能优化的思路 影响网页性能最大的因素是浏览器的重排(repaint)和重绘(reflow). React的Virtual DOM就是尽可能地减少浏览器的重排和重绘. 从React渲染过程来看,如何防 ...

  7. React拖拽组件Dragact V0.1.7:教你优化React组件性能与手感

    仓库地址:Dragact手感丝滑的拖拽布局组件 预览地址:支持手机端噢- 上回我们说到,Dragact组件已经进行了一系列的性能优化,然而面对大量数据的时候,依旧比较吃力,让我们来看看,优化之前的Dr ...

  8. 从卷积拆分和分组的角度看CNN模型的演化

    博客:博客园 | CSDN | blog 写在前面 如题,这篇文章将尝试从卷积拆分的角度看一看各种经典CNN backbone网络module是如何演进的,为了视角的统一,仅分析单条路径上的卷积形式. ...

  9. React组件性能优化

    转自:https://segmentfault.com/a/1190000006100489 React: 一个用于构建用户界面的JAVASCRIPT库. React仅仅专注于UI层:它使用虚拟DOM ...

随机推荐

  1. Apache网页的缓存时间

    配置网页缓存时间概述 通过mod_expires模块配置Apache,使网页能在客户端浏览器缓存一段时间,以避免重复请求,减轻服务端工作压力. 启用mod_expires模块后,会自动生成页面头部信息 ...

  2. Android权限解释

    属性 说明 android.permission.ACCESS_CHECKIN_PROPERTIES 允许读写访问 "properties"表在checkin数据库中,改值可以修改 ...

  3. WP8.1小梦词典开发2:百度翻译API使用

    原文出自:http://www.bcmeng.com/api2/ 小梦昨天和大家分享了WP8.1金山词霸API使用方法,今天继续分享windows phone 8.1中百度翻译API的使用方法.和昨天 ...

  4. 关于修改编JDK编译环境的问题

    最近从svn下拿的代码本地编译测试总是会出一个Unsupported major.minor version 51.0...的问题,出现这样的问题基本上都是因为JDK版本的问题,由于高版本JDK编译的 ...

  5. Struts2之Action与配置文件

    一.Struts2配置文件 1.struts.properties 在学习Action之前先学下Struts2的配置文件,与Struts2相关的配置文件有好几个,常用的有Struts.xml,web. ...

  6. (转)Nginx + uwsgi + web.py + MySQLdb

    一.背景知识: Web服务器上有多种方式运行Python: 传统上的CGI方式:基本已被淘汰 mod_python的模块方式:仍在广泛使用,但生产环境中也被逐步淘汰 uwsgi方式:目前生产环境下最受 ...

  7. iOS 文本转语音(TTS)详解:Swift

    上一篇博客讲解了iOS的speech FrameWork语音识别的功能:http://www.cnblogs.com/qian-gu-ling/p/6599670.html,对应的这篇博客就写一下文本 ...

  8. iOS面试必看经典试题分析

    > **不用临时变量怎么实现两个数据的交换?** 方式一:加减法的运算方式求解new_b = a - b + b = a;new_a = a + b - a = b;一个简单的运算方式,最重要的 ...

  9. Java实现缓存(LRU,FIFO)

    现在软件或者网页的并发量越来越大了,大量请求直接操作数据库会对数据库造成很大的压力,处理大量连接和请求就会需要很长时间,但是实际中百分之80的数据是很少更改的,这样就可以引入缓存来进行读取,减少数据库 ...

  10. C#研究OpenXML之路(4-使用第三方库)

    一.悲催的OpenXML操作 学习并使用了一段时间的OpenXML,感觉用这个来操作Excel文件真是一件非常悲催的事情,由于我自己比较熟悉Excel COM的操作,但到了OpenXML中发现,之前所 ...