React + Redux 渲染效能優化原理

發表於2016-09-23

React基本上成了前端的必備技能,redux更是對react的錦上添花。我在“redux,一種頁面狀態管理的優雅方案”一文中介紹了react與redux結合的基本方法和高階技巧。

大家都知道,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渲染等基本流程,可以通過下圖描述:

react-component-mount

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

(2)更新DOM

元件掛載到DOM後,一旦其props和state有更新,就會進入更新DOM流程。同樣我們也可以通過一張圖清晰的描述該流程的各個步驟:

react-component-update

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

(3)從DOM中解除安裝

從DOM中解除安裝的流程比較簡單,React只暴漏出componentWillUnmount,該函式使得我們可以在DOM解除安裝的最後時機對其進行干預。

2. react元件渲染效能監控

在進行效能優化前,我們先來了解如何對React元件渲染效能進行監控。React官方提供了Performance Tools,其使用起來也很簡單,通過Perf.start啟動一次效能分析,並通過Perf.stop結束一次效能分析。

呼叫Perf.stop後,我們就可以通過Perf提供的API來獲取本次效能分析的資料指標。其中最有用的API是Perf.printWasted(),其結果給出你在哪些元件上進行了無意義的(沒有引起真實DOM的改變)虛擬DOM比較,比如如下結果表明我們在TodoItem元件上浪費了4ms進行無意義的虛擬DOM比較,我們可以從這裡入手,進行效能優化。

react-perf-wasted

Perf.printInclusive()的結果則給出渲染各個元件的總體時間,通過它的結果我們可以找出哪個元件是頁面渲染的效能瓶頸。

react-perf-include

Perf.printInclusive()相似的API還有Perf.printExclusive(),只是其結果是元件渲染的獨佔時間,即不包括花費於載入元件的時間: 處理 props, getInitialState, 呼叫 componentWillMount 及 componentDidMount, 等等。

3. 效能優化基本原理

使用上一小節的效能分析工具,我們可以輕易的定位出哪些元件是頁面的效能瓶頸、哪些元件進行了無意義的虛擬DOM比較,本小節我們能探討如何對基於react和redux架構的前端應用進行效能優化。強烈的建議你參考我的另一篇博文:redux,一種頁面狀態管理的優雅方案,以便更好的理解本小節。

3.1 常規React元件效能優化

通過上文的React更新DOM流程,我們知道React提供了shouldComponentUpdate函式,它的結果直接影響元件是不是需要進行虛擬DOM比較以及後續的真實DOM渲染。而shouldComponentUpdate函式的預設返回值為true,這暗示著React總是會進行虛擬DOM比較,無論真實DOM是否需要重新渲染。我們可以通過根據自己的業務特性,過載shouldComponentUpdate,只在確認真實DOM需要改變時,再返回true。一般的做法是比較元件的props和state是否真的發生變化,如果發生變化則返回true,否則返回false。

進行深度比較(isDeepEqual)來確定props和state是否發生變化是最常見的做法,其是否有效能問題呢?如果一個容器型元件有很多的子節點,而子節點又有其他子節點,對這種複雜的巢狀物件進行深度比較(isDeepEqual)是很耗時的,甚至會抵消由避免虛擬DOM比較所帶來的效能收益。React官方推薦使用immutable的元件狀態,以便更高效的實現shouldComponentUpdate函式。

immutable的狀態有何優勢呢?假設我們要修改一個列表中,某個列表項的狀態,使用非immutable的方式:

當我們需要確認oldTodoList和newTodoList的資料是否相同時,只能遍歷列表(複雜度為O(n)),依次比較:

而如果使用immutable的方式:

因為每一次變動,都會建立新的物件,因此比較oldTodoList和newTodoList是否有變化時,只需要比較其物件引用即可(複雜度O(1)):

我們優化的方向就是將shouldComponentUpdate中所有的props和state的比較演算法複雜度降到最低,而淺層對比(isShallowEqual)就是複雜度最低的物件比較演算法:

當元件的prop設state都是immutable時,shouldComponentUpdate的實現就非常簡單了,我們可以直接使用facebook官方提供了PureRenderMixin,它就是對元件的props和state進行淺層比較的。

自己實現immutable化,還是很有挑戰的,我們可以藉助於第三方庫ImmutableJS,它是一個重型庫,適合於大型複雜專案;如果你的專案複雜度不是很高,可以使用seamless-immutable,它是一個更輕量級的庫,基於ES5的新特性Object.freeze來避免物件的修改,因此其只能相容實現ES5標準的瀏覽器。

3.2 理解Redux狀態傳播路徑

Redux使用一個物件儲存整個應用的狀態(global state),當global state發生變化時,狀態是如何傳遞的呢?這個問題的答案對我們理解基於redux的react應用的渲染效能優化至關重要。

Redux將React元件分為容器型元件和展示型元件。容器型元件一般通過connet函式生成,它訂閱了全域性狀態的變化,通過mapStateToProps函式,我們可以對全域性狀態進行過濾,只返回該容器型元件關注的區域性狀態:

每一次全域性狀態變化都會呼叫所有容器型元件的mapStateToProps方法,該方法返回一個常規的Javascript物件,並將其合併到容器型元件的props上。

而展示型元件不直接從global state獲取資料,其資料來源於父元件。當容器型元件對應global state有變化時,它會將變化傳播到其所有的子元件(一般為展示型元件)。簡單來說容器型元件與展示型元件是父子關係:

元件型別 資料來源 變化通知
展示型元件 父元件 父元件通知
容器型元件 全域性狀態 監聽全域性狀態

元件的狀態傳遞路徑,可以用一個樹形結構描述:

redux-component-tree

3.3 理解Redux的預設效能優化

Redux官方對容器型元件和全域性狀態樹有兩個基本的假設,違背這些假設將使得Redux的預設效能優化無法起作用:

這種規範是有理由的:上文中我們提到過,每一次全域性狀態發生變化,所有的容器型元件都會得到通知,而各個容器型元件需要通過shouldComponentUpdate函式來確實自己關注的區域性狀態是否發生變化、自身是否需要重新渲染,預設情況下,React元件的shouldComponentUpdate總返回true,這裡貌似有一個嚴重的效能問題:全域性狀態的任何變動都會使頁面中的所有元件進入更新DOM的流程

幸運的是,用Redux官方API函式connect生成的容器型元件,預設會提供一個shouldComponentUpdate函式,其中對props和state進行了淺層比較`。如果我們不遵從Redux的immutable狀態的規範和Pure Component規範,則容器型元件預設的shouldComponentUpdate函式就是無效的了。

在遵從Redux的immutable狀態規範的情況下,當一個容器型元件的預設shouldComponentUpdate函式返回true時,則表明其對應的區域性狀態發生變化,需要將狀態傳播到各個子元件,相應的所有子元件也都會進行虛擬DOM比較,以確定是否需要重新渲染。如下圖所示,容器型元件#1的狀態發生變化後,所有的子元件都會進行虛擬DOM比較:

redux-performance-loose

由於展示型元件對全域性狀態沒有感知,我們就可以使用React的常規方法對展示型進行渲染效能優化了。使用小節3.1中所提到的常規React元件效能優化方案,對每一個展示型元件實現shouldComponentUpdate函式:

我們就可以避免展示型元件多餘的虛擬DOM比較。比如當只有展示型元件#1.1需要重新渲染時,其他同級別的元件不會進行虛擬DOM比較。比如當只有展示型元件#1.1需要重新渲染時,其他同級別的元件不會進行虛擬DOM比較了

redux-performance-best

綜上所述: 在容器型元件層面,Redux為我們提供了預設的效能優化方案;在展示型元件層面,我們可以使用常規React元件效能優化方案。


參考文獻:

https://facebook.github.io/react/docs/component-specs.html

https://facebook.github.io/react/docs/perf.html

http://benchling.engineering/performance-engineering-with-react/

http://jlongster.com/Using-Immutable-Data-Structures-in-JavaScript

https://github.com/rtfeldman/seamless-immutable

https://facebook.github.io/react/docs/pure-render-mixin.html

https://facebook.github.io/react/docs/advanced-performance.html

https://www.toptal.com/react/react-redux-and-immutablejs

https://facebook.github.io/react/docs/advanced-performance.html

相關文章