从性能角度看react组件拆分的重要性
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的过程,否则需要更新。
具体可以这么显示实现:
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。
从图可以看出我们在输入单个字符时,输入框的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可以避免不必要的组件更新。
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组件拆分的重要性的更多相关文章
- 从源码的角度看 React JS 中批量更新 State 的策略(下)
这篇文章我们继续从源码的角度学习 React JS 中的批量更新 State 的策略,供我们继续深入学习研究 React 之用. 前置文章列表 深入理解 React JS 中的 setState 从源 ...
- 从源码的角度看 React JS 中批量更新 State 的策略(上)
在之前的文章「深入理解 React JS 中的 setState」与 「从源码的角度再看 React JS 中的 setState」 中,我们分别看到了 React JS 中 setState 的异步 ...
- React 组件性能优化探索实践
转自:http://www.tuicool.com/articles/Ar6Zruq React本身就非常关注性能,其提供的虚拟DOM搭配上Diff算法,实现对DOM操作最小粒度的改变也是非常的高效. ...
- React 组件性能优化
React组件性能优化 前言 众所周知,浏览器的重绘和重排版(reflows & repaints)(DOM操作都会引起)才是导致网页性能问题的关键.而React虚拟DOM的目的就是为了减少浏 ...
- 从工程化角度讨论如何快速构建可靠React组件
前言 React 的开发也已经有2年时间了,先从QQ的家校群,转成做互动直播,主要是花样直播这一块.切换过来的时候,业务非常繁忙,接手过来的业务比较凌乱,也没有任何组件复用可言. 为了提高开发效率,去 ...
- React组件性能优化总结
性能优化的思路 影响网页性能最大的因素是浏览器的重排(repaint)和重绘(reflow). React的Virtual DOM就是尽可能地减少浏览器的重排和重绘. 从React渲染过程来看,如何防 ...
- React拖拽组件Dragact V0.1.7:教你优化React组件性能与手感
仓库地址:Dragact手感丝滑的拖拽布局组件 预览地址:支持手机端噢- 上回我们说到,Dragact组件已经进行了一系列的性能优化,然而面对大量数据的时候,依旧比较吃力,让我们来看看,优化之前的Dr ...
- 从卷积拆分和分组的角度看CNN模型的演化
博客:博客园 | CSDN | blog 写在前面 如题,这篇文章将尝试从卷积拆分的角度看一看各种经典CNN backbone网络module是如何演进的,为了视角的统一,仅分析单条路径上的卷积形式. ...
- React组件性能优化
转自:https://segmentfault.com/a/1190000006100489 React: 一个用于构建用户界面的JAVASCRIPT库. React仅仅专注于UI层:它使用虚拟DOM ...
随机推荐
- Linux(CentOS6.7) 安装MySql5.7数据库
linux(CentOS6.7) 环境Mysql 5.7.17安装教程分享给大家,供大家参考,具体内容如下: 1系统约定安装文件下载目录:/data/softwareMysql目录安装位置:/usr/ ...
- 谈谈数据库中MyISAM与InnoDB区别 针对业务类型选择合适的表
MyISAM:这个是默认类型,它是基于传统的ISAM类型, ISAM是Indexed Sequential Access Method (有索引的顺序访问方法) 的缩写,它是存储记录和文件的标准方法. ...
- Java的一些基础知识
1.do-while循环不论循环条件判断结果,至少会执行一次. 2.Javac用于Java源代码文件编译成字节码的编译器. 3.import关键字导入包. 4.Java是Sun公司与1995年退出的高 ...
- JS 拖动DIV 需要JQUERY 支持
<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- CentOs6系统安装mailx发邮件
1. yum -y mail* sendmail* postfix* service sendmail start 2. cp /etc/mail.rc /etc/mail.rc.bak cat &g ...
- ThinkPHP3.2.3版本验证码异步第二次验证时失败的问题解决
最近在用TP3.2.3做一个小项目,纠结于验证码验证问题,重点在于二次验证,举个例子就是常见的登录页面上有个验证码输入框,当用户输入验证码并且鼠标点击在这个输入框之外时候,触发onblur事件,然后a ...
- vmware克隆之后网卡起不来的问题
问题: 克隆一台主机之后,改主机的网卡起不来,只有一个本地的回环地址网卡. 使用如下的命令都无效. /etc/init.d/network restart ifup eth0 原因: 这一vmware ...
- Python_入门
本章内容: 1.Python的种类 2.Python的环境 3.Python入门(解释器.编码.pyc文件.脚步传入参数.变量.输入.流程控制与缩进.while循环) 4.练习题 Python的种类 ...
- Alamofire源码解读系列(十一)之多表单(MultipartFormData)
本篇讲解跟上传数据相关的多表单 前言 我相信应该有不少的开发者不明白多表单是怎么一回事,然而事实上,多表单确实很简单.试想一下,如果有多个不同类型的文件(png/txt/mp3/pdf等等)需要上传给 ...
- 【机器学习】代价函数(cost function)
注:代价函数(有的地方也叫损失函数,Loss Function)在机器学习中的每一种算法中都很重要,因为训练模型的过程就是优化代价函数的过程,代价函数对每个参数的偏导数就是梯度下降中提到的梯度,防止过 ...