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. idea 中设置成公司规范的代码格式

    优雅的编码格式是一个程序员的必备素质. 最近切换到了 idea,想对自己的代码进行格式化的时候希望能自动排版成公司规定的格式,可以做以下设置: 打开 idea 的 preference: 左侧找到 c ...

  2. gcc下c++的对象模型 (1)

    所有示例代码在如下环境中执行 ubuntu 16.04.4 (64位) gcc version 5.4.0 开启std11 gdb version 7.11.1 1. 空类的大小 定义一个空类A,实例 ...

  3. 基于微软开发平台构建和使用私有NuGet托管库

    本篇blog包含使用TFS2017,VS2017等平台和工具搭建和使用NuGet库等基本过程,为团体提供更加自动化和高效的研发活动支持. 作为以产品线或者以专属业务为扩展的项目类型的软件研发团体,都会 ...

  4. Hive分区(静态分区+动态分区)

    Hive分区的概念与传统关系型数据库分区不同. 传统数据库的分区方式:就oracle而言,分区独立存在于段里,里面存储真实的数据,在数据进行插入的时候自动分配分区. Hive的分区方式:由于Hive实 ...

  5. Activiti的全局事件机制及其监听处理

    概述 Activiti在5.15以后的版本后,增加了统一的事件入口,不需要再像以前那样,监听流程的事件时,在流程定义的BPMN文件中为每个节点及流程增加以下的配置,以实现监听事件的做法,这种做法导致我 ...

  6. Java类修饰符

  7. css实现超出部分用...代替

    如果是一行的话 css为 white-space: nowrap: 保证文本内容不会自动换行,如果多余的内容会在水平方向撑破单元格. overflow: hidden: 隐藏超出单元格的部分. tex ...

  8. Xcode的Architectures、Valid Architectures和Build Active Architecture Only属性(原创)

    最近xcode升级了5.1版本,升级之后程序报关于要适配arm64机器的错.之前对xcode的参数配置,一直不是很了解,但实现先面对问题了,就调查了一下并解决它. 一个一个来吧. Architectu ...

  9. JavaWeb总结(三)—JSP

    一.JSP简介 1.基本认识 (1)JSP页面是由HTML语句和嵌套在其中的Java代码组成的一个普通文本文件,JSP 页面的文件扩展名必须为.jsp. (2)在JSP页面中编写的Java代码需要嵌套 ...

  10. Python JavaScript概述

    一.如何编写? 1.JavaScript代码存在形式 <!DOCTYPE html> <html> <head> <meta http-equiv=" ...