你们都知道,react的一个痛点就是非父子关系的组件之间的通讯,其官方文档对此也并不避讳:javascript
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.html
而redux就能够视为其中的“global event system”,使用redux可使得咱们的react应用有更加清晰的架构。前端
本文咱们来探讨,基于react和redux架构的前端应用,如何进行渲染性能优化。对于小型react前端应用,最好的优化就是不优化
由于React自己就是经过比较虚拟DOM的差别,从而对真实DOM进行最小化操做,小型React应用的虚拟DOM结构简单,虚拟DOM比较的耗时能够忽略不计。而对于复杂的前端项目,咱们所指的渲染性能优化,其实是指,在不须要更新DOM时,如何避免虚拟DOM的比较
。java
工欲善其事,必先利其器。理解react的组件的生命周期是优化其渲染性能的必备前提。咱们能够将react组件的生命周期分为3个大循环:挂载到DOM、更新DOM、从DOM中卸载。React对三个大循环中每一步都暴露出钩子函数,使得咱们能够细粒度地控制组件的生命周期。react
组件首次插入到DOM时,会经历从属性和状态初始化到DOM渲染等基本流程,能够经过下图描述:git
必须注意的是,挂载到DOM流程在组件的整个生命周期只有一次,也就是组件第一次插入DOM文档流时。在挂载到DOM流程中的每一步也有相应的限制:github
getDefaultProps()和getInitialState()中不能获取和设置组件的state。
render()方法中不能设置组件的state。
组件挂载到DOM后,一旦其props和state有更新,就会进入更新DOM流程。一样咱们也能够经过一张图清晰的描述该流程的各个步骤:算法
componentWillReceiveProps()提供了该流程中更新state的最后时机,后续的其余函数都不能再更新组件的state了。咱们尤为须要注意的是shouldComponentUpdate函数,它的结果直接影响该组件是否是须要进行虚拟DOM比较
,咱们对组件渲染性能优化的基本思路就是:在非必要的时候将shouldComponentUpdate返回值设置为false,从而终止更新DOM流程中的后续步骤。redux
从DOM中卸载的流程比较简单,React只暴漏出componentWillUnmount
,该函数使得咱们能够在DOM卸载的最后时机对其进行干预。浏览器
在进行性能优化前,咱们先来了解如何对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, 等等。
使用上一小节的性能分析工具,咱们能够轻易的定位出哪些组件是页面的性能瓶颈、哪些组件进行了无心义的虚拟DOM比较,本小节咱们能探讨如何对基于react和redux架构的前端应用进行性能优化。
经过上文的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标准的浏览器。
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
有变化时,它会将变化传播到其全部的子组件(通常为展现型组件)。简单来讲容器型组件与展现型组件是父子关系:
组件类型 | 数据来源 | 变化通知 |
---|---|---|
展现型组件 | 父组件 | 父组件通知 |
容器型组件 | 全局状态 | 监听全局状态 |
组件的状态传递路径,能够用一个树形结构描述:
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组件性能优化方案。