大家都知道,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渲染性能优化原理的更多相关文章
-
相当有用的react+redux渲染性能优化原理
学习地址:http://foio.github.io/react-redux-performance-boost/
-
React + Reflux 渲染性能优化原理
作者:ManfredHu 链接:http://www.manfredhu.com/2016/11/08/23-reactRenderingPrinciple 声明:版权所有,转载请保留本段信息,否则请 ...
-
Android性能优化之UI渲染性能优化
版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 本篇博客主要记录一些工作中常用的UI渲染性能优化及调试方法,理解这些方法对于我们编写高质量代码也是有一些帮助的,主要内容包括介绍CPU,GPU的职 ...
-
react 实用的性能优化方式
react 组件渲染分为初始化渲染和更新渲染,当我们更新某个组件的时候,只是想关键路径上组件的render,但react的默认做法是调用所以组件的reder,再生成虚拟dom进行对比,如不变则不进行更 ...
-
android app性能优化大汇总(UI渲染性能优化)
UI性能测试 性能优化都需要有一个目标,UI的性能优化也是一样.你可能会觉得“我的app加载很快”很重要,但我们还需要了解终端用户的期望,是否可以去量化这些期望呢?我们可以从人机交互心理学的角度来考虑 ...
-
reselect是怎样提升react组件渲染性能的?
reselect是什么? reselect是配合redux使用的一款轻量型的状态选择库,目的在于当store中的state重新改变之后,使得局部未改变的状态不会因为整体的state变化而全部重新渲染, ...
-
react 首屏性能优化
首屏优化点:1.加载包(bundle.js)文件的大小,越小,首屏渲染速度越快 (按需加载) 2.优先渲染用户直观看到的页面部分(懒加载) 技术点:react-loadable . react-laz ...
-
React性能优化之PureComponent 和 memo使用分析
前言 关于react性能优化,在react 16这个版本,官方推出fiber,在框架层面优化了react性能上面的问题.由于这个太过于庞大,我们今天围绕子自组件更新策略,从两个及其微小的方面来谈rea ...
-
Js基础知识(五) - 前端性能优化总结
前端性能优化总结 资源优化 缓存 最好的资源优化就是不加载资源.缓存也是最见效的优化手段.说实话,虽然说客户端缓存发生在浏览器端,但缓存主要还是服务端来控制,与我们前端关系并不是很大.但还是有必要了解 ...
随机推荐
-
Elasticsearch——分页查询From&;Size VS scroll
Elasticsearch中数据都存储在分片中,当执行搜索时每个分片独立搜索后,数据再经过整合返回.那么,如果要实现分页查询该怎么办呢? 更多内容参考Elasticsearch资料汇总 按照一般的查询 ...
-
数据结构之链表、栈和队列 java代码实现
定义抽象节点类Node: package cn.wzbrilliant.datastructure; /** * 节点 * @author ice * */ public abstract class ...
-
【蒟蒻の进阶PLAN】 置顶+持续连载
看到周围神犇们纷纷列计划,本蒟蒻也决定跟随他们的步伐,计划大约是周计划吧,具体怎么安排我也不确定.. 2015.12.30 刚刚学习完最基础的网络流,需要进行这方面的练习,从简到难,有空余的话尝试学习 ...
-
Is an MTD device a block device or a char device?
转:http://www.linux-mtd.infradead.org/faq/general.html#L_mtd_what Note, you can find Ukranian transla ...
-
一个C#的XML数据库访问类
原文地址:http://hankjin.blog.163.com/blog/static/33731937200942915452244/ 程序中不可避免的要用到配置文件或数据,对于数据量比较小的程序 ...
-
回溯算法————n皇后、素数串
回溯就是算法是搜索算法中一种控制策略,是一个逐个试探的过程.在试探的过程中,如果遇到错误的选择,就会回到上一步继续选择下一种走法,一步一步的进行直到找到解或者证明无解为止. 如下是一个经典回溯问题n皇 ...
-
Linux系统安装workerman,启动wss 服务
安装workerman其实很简单,只要会简单的linux口令就可以搞定, 这里我给大家演示一下如何安装workerman 进入终端的过程就不用演示了吧... 输入root及密码进入终端后找到站点根目录 ...
-
Oracle活动会话历史(ASH)及报告解读
对于数据库运行期间的各种状态的实时监控以及相关性能数据捕获对于解决性能问题,提高整体业务系统运行效率是至关重要的.在Oracle数据库中,实时捕获相关性能数据是通过ASH工具来实现的.ASH通过每秒钟 ...
-
Subquery typo with using in(转)
Subquery typo with using in Do you use the following syntax? SELECT * FROM TABLE WHERE COLUMN IN ( ...
-
d3js path generator vs layouts
我们知道d3的一般套路就是d3.selectAll('path.mypath').data(yourDataset).enter().append('path').attr('class','mypa ...