react+redux渲染性能優化原理

你們都知道,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

1. react組件的生命週期

工欲善其事,必先利其器。理解react的組件的生命週期是優化其渲染性能的必備前提。咱們能夠將react組件的生命週期分爲3個大循環:掛載到DOM、更新DOM、從DOM中卸載。React對三個大循環中每一步都暴露出鉤子函數,使得咱們能夠細粒度地控制組件的生命週期。react

(1)掛載到DOM

組件首次插入到DOM時,會經歷從屬性和狀態初始化到DOM渲染等基本流程,能夠經過下圖描述:git

render flow

必須注意的是,掛載到DOM流程在組件的整個生命週期只有一次,也就是組件第一次插入DOM文檔流時。在掛載到DOM流程中的每一步也有相應的限制:github

getDefaultProps()和getInitialState()中不能獲取和設置組件的state。
render()方法中不能設置組件的state。

(2)更新DOM

組件掛載到DOM後,一旦其props和state有更新,就會進入更新DOM流程。一樣咱們也能夠經過一張圖清晰的描述該流程的各個步驟:算法

render flow

componentWillReceiveProps()提供了該流程中更新state的最後時機,後續的其餘函數都不能再更新組件的state了。咱們尤爲須要注意的是shouldComponentUpdate函數,它的結果直接影響該組件是否是須要進行虛擬DOM比較,咱們對組件渲染性能優化的基本思路就是:在非必要的時候將shouldComponentUpdate返回值設置爲false,從而終止更新DOM流程中的後續步驟。redux

(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比較,咱們能夠從這裏入手,進行性能優化。

Alt text

Perf.printInclusive()的結果則給出渲染各個組件的整體時間,經過它的結果咱們能夠找出哪一個組件是頁面渲染的性能瓶頸。

Alt text

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有變化時,它會將變化傳播到其全部的子組件(通常爲展現型組件)。簡單來講容器型組件與展現型組件是父子關係:

組件類型 數據來源 變化通知
展現型組件 父組件 父組件通知
容器型組件 全局狀態 監聽全局狀態

組件的狀態傳遞路徑,能夠用一個樹形結構描述:

Alt text

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比較:

Alt text

因爲展現型組件對全局狀態沒有感知,咱們就可使用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比較了

Alt text

結語: 在容器型組件層面,Redux爲咱們提供了默認的性能優化方案;在展現型組件層面,咱們可使用常規React組件性能優化方案。