大家都知道,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組件性能優化方案。
