大家都知道,react的一个痛点就是非父子关系的组件之间的通信,其官方文档对此也并不避讳:

For communication between two components that don't have a parent-child relationship, you can set up your own global event system. Subscribe to events in componentDidMount(), unsubscribe in componentWillUnmount(), and call setState() when you receive an event.

而redux就可以视为其中的“global event system”,使用redux可以使得我们的react应用有更加清晰的架构。

本文我们来探讨,基于react和redux架构的前端应用,如何进行渲染性能优化。对于小型react前端应用,最好的优化就是不优化因为React本身就是通过比较虚拟DOM的差异,从而对真实DOM进行最小化操作,小型React应用的虚拟DOM结构简单,虚拟DOM比较的耗时可以忽略不计。而对于复杂的前端项目,我们所指的渲染性能优化,实际上是指,在不需要更新DOM时,如何避免虚拟DOM的比较

1. react组件的生命周期

工欲善其事,必先利其器。理解react的组件的生命周期是优化其渲染性能的必备前提。我们可以将react组件的生命周期分为3个大循环:挂载到DOM、更新DOM、从DOM中卸载。React对三个大循环中每一步都暴露出钩子函数,使得我们可以细粒度地控制组件的生命周期。

(1)挂载到DOM

组件首次插入到DOM时,会经历从属性和状态初始化到DOM渲染等基本流程,可以通过下图描述:

必须注意的是,挂载到DOM流程在组件的整个生命周期只有一次,也就是组件第一次插入DOM文档流时。在挂载到DOM流程中的每一步也有相应的限制:

getDefaultProps()和getInitialState()中不能获取和设置组件的state。
render()方法中不能设置组件的state。

(2)更新DOM

组件挂载到DOM后,一旦其props和state有更新,就会进入更新DOM流程。同样我们也可以通过一张图清晰的描述该流程的各个步骤:

componentWillReceiveProps()提供了该流程中更新state的最后时机,后续的其他函数都不能再更新组件的state了。我们尤其需要注意的是shouldComponentUpdate函数,它的结果直接影响该组件是不是需要进行虚拟DOM比较,我们对组件渲染性能优化的基本思路就是:在非必要的时候将shouldComponentUpdate返回值设置为false,从而终止更新DOM流程中的后续步骤。

(3)从DOM中卸载

从DOM中卸载的流程比较简单,React只暴漏出componentWillUnmount,该函数使得我们可以在DOM卸载的最后时机对其进行干预。

2. react组件渲染性能监控

在进行性能优化前,我们先来了解如何对React组件渲染性能进行监控。React官方提供了Performance Tools,其使用起来也很简单,通过Perf.start启动一次性能分析,并通过Perf.stop结束一次性能分析。

import Perf from 'react-addons-perf'
Perf.start();
....your react code
Perf.stop();

调用Perf.stop后,我们就可以通过Perf提供的API来获取本次性能分析的数据指标。其中最有用的API是Perf.printWasted(),其结果给出你在哪些组件上进行了无意义的(没有引起真实DOM的改变)虚拟DOM比较,比如如下结果表明我们在TodoItem组件上浪费了4ms进行无意义的虚拟DOM比较,我们可以从这里入手,进行性能优化。

Perf.printInclusive()的结果则给出渲染各个组件的总体时间,通过它的结果我们可以找出哪个组件是页面渲染的性能瓶颈。

Perf.printInclusive()相似的API还有Perf.printExclusive(),只是其结果是组件渲染的独占时间,即不包括花费于加载组件的时间: 处理 props, getInitialState, 调用 componentWillMount 及 componentDidMount, 等等。

3. 性能优化基本原理

使用上一小节的性能分析工具,我们可以轻易的定位出哪些组件是页面的性能瓶颈、哪些组件进行了无意义的虚拟DOM比较,本小节我们能探讨如何对基于react和redux架构的前端应用进行性能优化。

3.1 常规React组件性能优化

通过上文的React更新DOM流程,我们知道React提供了shouldComponentUpdate函数,它的结果直接影响组件是不是需要进行虚拟DOM比较以及后续的真实DOM渲染。而shouldComponentUpdate函数的默认返回值为true,这暗示着React总是会进行虚拟DOM比较,无论真实DOM是否需要重新渲染。我们可以通过根据自己的业务特性,重载shouldComponentUpdate,只在确认真实DOM需要改变时,再返回true。一般的做法是比较组件的props和state是否真的发生变化,如果发生变化则返回true,否则返回false。

 shouldComponentUpdate: function (nextProps, nextState) {
return !isDeepEqual(this.props,nextProps) || !isDeepEqual(this.state,nextState);
}

进行深度比较(isDeepEqual)来确定props和state是否发生变化是最常见的做法,其是否有性能问题呢?如果一个容器型组件有很多的子节点,而子节点又有其他子节点,对这种复杂的嵌套对象进行深度比较(isDeepEqual)是很耗时的,甚至会抵消由避免虚拟DOM比较所带来的性能收益。React官方推荐使用immutable的组件状态,以便更高效的实现shouldComponentUpdate函数。

immutable的状态有何优势呢?假设我们要修改一个列表中,某个列表项的状态,使用非immutable的方式:

var item = {
id:1,
text:'todo1',
status:'doing'
}
var oldTodoList = [item1,item2,....,itemn];
oldTodoList[n-1].status = 'done';
var newTodoList = oldTotoList;

当我们需要确认oldTodoList和newTodoList的数据是否相同时,只能遍历列表(复杂度为O(n)),依次比较:

for(var i = 0; i < oldTodoList.length; i++){
if(isItemEqual(oldTodoList[i],newTodoList[i])){
return true;
}
}
return false;

而如果使用immutable的方式:

var newTotoList = oldTodoList.map(function(item){
if(item.id == n-1){
return Object.assign({},item,{status:'done'})
}else{
return item;
}
});

因为每一次变动,都会创建新的对象,因此比较oldTodoList和newTodoList是否有变化时,只需要比较其对象引用即可(复杂度O(1)):

return oldTodoList == newTodoList;

我们优化的方向就是将shouldComponentUpdate中所有的props和state的比较算法复杂度降到最低,而浅层对比(isShallowEqual)就是复杂度最低的对象比较算法:

 shouldComponentUpdate: function (nextProps, nextState) {
return !isShallowEqual(this.props,nextProps) || !isShallowEqual(this.state,nextState);
}

当组件的prop设state都是immutable时,shouldComponentUpdate的实现就非常简单了,我们可以直接使用facebook官方提供了PureRenderMixin,它就是对组件的props和state进行浅层比较的。

var PureRenderMixin = require('react-addons-pure-render-mixin');
React.createClass({
mixins: [PureRenderMixin], render: function() {
return <div className={this.props.className}>foo</div>;
}
});

自己实现immutable化,还是很有挑战的,我们可以借助于第三方库ImmutableJS,它是一个重型库,适合于大型复杂项目;如果你的项目复杂度不是很高,可以使用seamless-immutable,它是一个更轻量级的库,基于ES5的新特性Object.freeze来避免对象的修改,因此其只能兼容实现ES5标准的浏览器。

3.2 理解Redux状态传播路径

Redux使用一个对象存储整个应用的状态(global state),当global state发生变化时,状态是如何传递的呢?这个问题的答案对我们理解基于redux的react应用的渲染性能优化至关重要。

Redux将React组件分为容器型组件和展示型组件。容器型组件一般通过connet函数生成,它订阅了全局状态的变化,通过mapStateToProps函数,我们可以对全局状态进行过滤,只返回该容器型组件关注的局部状态:

function mapStateToProps(state) {
return {todos: state.todos};
}
module.exports = connect(mapStateToProps)(TodoApp);

每一次全局状态变化都会调用所有容器型组件的mapStateToProps方法,该方法返回一个常规的Javascript对象,并将其合并到容器型组件的props上。

而展示型组件不直接从global state获取数据,其数据来源于父组件。当容器型组件对应global state有变化时,它会将变化传播到其所有的子组件(一般为展示型组件)。简单来说容器型组件与展示型组件是父子关系:

组件类型 数据来源 变化通知
展示型组件 父组件 父组件通知
容器型组件 全局状态 监听全局状态

组件的状态传递路径,可以用一个树形结构描述:

3.3 理解Redux的默认性能优化

Redux官方对容器型组件和全局状态树有两个基本的假设,违背这些假设将使得Redux的默认性能优化无法起作用:

1. 容器型组件必须为Pure Component,即组件只依赖于state和props
2. 全局状态树(global state)的任何变动都是immutable的

这种规范是有理由的:上文中我们提到过,每一次全局状态发生变化,所有的容器型组件都会得到通知,而各个容器型组件需要通过shouldComponentUpdate函数来确实自己关注的局部状态是否发生变化、自身是否需要重新渲染,默认情况下,React组件的shouldComponentUpdate总返回true,这里貌似有一个严重的性能问题:全局状态的任何变动都会使页面中的所有组件进入更新DOM的流程

幸运的是,用Redux官方API函数connect生成的容器型组件,默认会提供一个shouldComponentUpdate函数,其中对props和state进行了浅层比较`。如果我们不遵从Redux的immutable状态的规范和Pure Component规范,则容器型组件默认的shouldComponentUpdate函数就是无效的了。

在遵从Redux的immutable状态规范的情况下,当一个容器型组件的默认shouldComponentUpdate函数返回true时,则表明其对应的局部状态发生变化,需要将状态传播到各个子组件,相应的所有子组件也都会进行虚拟DOM比较,以确定是否需要重新渲染。如下图所示,容器型组件#1的状态发生变化后,所有的子组件都会进行虚拟DOM比较:

由于展示型组件对全局状态没有感知,我们就可以使用React的常规方法对展示型进行渲染性能优化了。使用小节3.1中所提到的常规React组件性能优化方案,对每一个展示型组件实现shouldComponentUpdate函数:

 shouldComponentUpdate: function (nextProps, nextState) {
return !isShallowEqual(this.props,nextProps) || !isShallowEqual(this.state,nextState);
}

我们就可以避免展示型组件多余的虚拟DOM比较。比如当只有展示型组件#1.1需要重新渲染时,其他同级别的组件不会进行虚拟DOM比较。比如当只有展示型组件#1.1需要重新渲染时,其他同级别的组件不会进行虚拟DOM比较了

结语: 在容器型组件层面,Redux为我们提供了默认的性能优化方案;在展示型组件层面,我们可以使用常规React组件性能优化方案。

react+redux渲染性能优化原理的更多相关文章

  1. 相当有用的react+redux渲染性能优化原理

    学习地址:http://foio.github.io/react-redux-performance-boost/

  2. React + Reflux 渲染性能优化原理

    作者:ManfredHu 链接:http://www.manfredhu.com/2016/11/08/23-reactRenderingPrinciple 声明:版权所有,转载请保留本段信息,否则请 ...

  3. Android性能优化之UI渲染性能优化

    版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 本篇博客主要记录一些工作中常用的UI渲染性能优化及调试方法,理解这些方法对于我们编写高质量代码也是有一些帮助的,主要内容包括介绍CPU,GPU的职 ...

  4. react 实用的性能优化方式

    react 组件渲染分为初始化渲染和更新渲染,当我们更新某个组件的时候,只是想关键路径上组件的render,但react的默认做法是调用所以组件的reder,再生成虚拟dom进行对比,如不变则不进行更 ...

  5. android app性能优化大汇总(UI渲染性能优化)

    UI性能测试 性能优化都需要有一个目标,UI的性能优化也是一样.你可能会觉得“我的app加载很快”很重要,但我们还需要了解终端用户的期望,是否可以去量化这些期望呢?我们可以从人机交互心理学的角度来考虑 ...

  6. reselect是怎样提升react组件渲染性能的?

    reselect是什么? reselect是配合redux使用的一款轻量型的状态选择库,目的在于当store中的state重新改变之后,使得局部未改变的状态不会因为整体的state变化而全部重新渲染, ...

  7. react 首屏性能优化

    首屏优化点:1.加载包(bundle.js)文件的大小,越小,首屏渲染速度越快 (按需加载) 2.优先渲染用户直观看到的页面部分(懒加载) 技术点:react-loadable . react-laz ...

  8. React性能优化之PureComponent 和 memo使用分析

    前言 关于react性能优化,在react 16这个版本,官方推出fiber,在框架层面优化了react性能上面的问题.由于这个太过于庞大,我们今天围绕子自组件更新策略,从两个及其微小的方面来谈rea ...

  9. Js基础知识(五) - 前端性能优化总结

    前端性能优化总结 资源优化 缓存 最好的资源优化就是不加载资源.缓存也是最见效的优化手段.说实话,虽然说客户端缓存发生在浏览器端,但缓存主要还是服务端来控制,与我们前端关系并不是很大.但还是有必要了解 ...

随机推荐

  1. [顶]ORACLE PL/SQL编程详解之二:PL/SQL块结构和组成元素(为山九仞,岂一日之功)

    原文:[顶]ORACLE PL/SQL编程详解之二:PL/SQL块结构和组成元素(为山九仞,岂一日之功) [顶]ORACLE PL/SQL编程详解之二: PL/SQL块结构和组成元素(为山九仞,岂一日 ...

  2. 为Pythonic论坛添加一个“专题”功能(续)

    上篇博文<为Pythonic论坛添加一个“专题”功能>,在模板的层次上对发帖进行了限制.也就是根据用户是否拥有权限来决定是否显示发帖框. 但是自从这么“投机取巧”的写完模板后,整夜辗转反侧 ...

  3. [译]Java 设计模式之组合

    (文章翻译自Java Design Pattern: Composite) 组合模式相对来说是比较简单的,但是它在很多设计中被用到,比如SWT.Eclipse工作空间等等.它创建了一个可以通过唯一的方 ...

  4. firefox os 该设备呼叫移动开发

    1)话筒 权限:telephony api:navigator.moztelephony 参考链接:https://wiki.mozilla.org/WebAPI/WebTelephony 2) 扬声 ...

  5. hdu 1059 Dividing 多重背包

    点击打开链接链接 Dividing Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others ...

  6. socket计划——一个简单的例子

    从一个简单易用TCP样品开始socket计划,的基本过程例如下列: server                                                  client +++ ...

  7. mvc4项目数据库优先的尝试

    对于mvc代码优先原则,感觉真不知道为什么硬要设计这种模式,代码优先使得每次运行程序都要重建数据库,现实中这种模式有什么用呢. 数据库优先可能有好多方式,看了好久才做出来一种比较简单的.通过先添加一个 ...

  8. 网际协议:无连接数据报交付(IPv4)

    一.数据报格式:

  9. C++ string和c类型字符数组的比较

    在c++中string是很方便操作的字符串,支持多种算数运算和比较运算,操作起来非常灵活.string也具有一些容器的性质,可以通过迭代器对字符元素进行访问 c类型的字符数组有如下三种初始化方式: / ...

  10. Linux环境进程间通信(五): 共享内存(上)

    linux下进程间通信的几种主要手段: 管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允 ...