React+Redux 性能優(yōu)化實踐?
極牛技術實踐分享活動
極牛技術實踐分享系列活動是極牛聯(lián)合頂級VC、技術專家,為企業(yè)、技術人提供的一種系統(tǒng)的線上技術分享活動。
每期不同的技術主題,和行業(yè)專家深度探討,專注解決技術實踐難點,推動技術創(chuàng)新,每兩周的周三20點正式開課。歡迎各個機構、企業(yè)、行業(yè)專家、技術人報名參加。
本期大綱
Redux +React是怎么一個運行機制;
如何使用這套框架來構建我們的網(wǎng)站;
在使用這套框架的過程中我們可能會踩到哪些坑,我們的性能會受到哪些影響。
嘉賓介紹
劉華清,GrowingIO核心工程師,負責GrowingIO整個前端工程的搭建。
北郵研究生,畢業(yè)后在微軟 Office 部門工作 2 年,參與開發(fā)了 Office App 的整套 Ecosystem,以及 Office Word 的 Web 版本的相關 feature。對于前后端開發(fā)、軟件工程、數(shù)據(jù)分析有自己較深刻的理解。
本期分享中劉老師提供了非常詳細的資料,介紹了什么是React+Redux,作者從為什么要使用它到如何去使用它,以及再到解決實際問題所給出的方案。并且可以洞悉在開發(fā)中例如網(wǎng)站為何越來越慢等問題的癥結所在,同時可以很清晰明了的理解React+Redux所具有的優(yōu)勢,以提高數(shù)據(jù)層計算的效率同時可以更智能化的避免不必要的Component渲染。圖片描述
首先我們來看一下,什么是『Redux + React』。我們都知道前端的發(fā)展最近幾年非常迅速,每一年都會有很多新技術出現(xiàn)。比如最近幾年的 AngularJS,Vue,React+Redux。
這技術其中最核心的一個概念是數(shù)據(jù)綁定,記得沒錯的話最早提出數(shù)據(jù)綁定的應該是微軟的WPF 技術,后來微軟的一個工程師把雙向概念應用到前端開發(fā),并寫出了Knockout 雙向綁定框架。當時從jQuery轉到使用數(shù)據(jù)綁定的概念的時候,感覺真的不可思議,太好用了。
后來就不斷有更多的框架地采用數(shù)據(jù)綁定的概念,然后使得前端開發(fā)會越來越容易,整個的框架也有越來越清晰。
圖片描述
這里『Redux + React』也同樣使用了數(shù)據(jù)綁定的概念,但它用的是單向數(shù)據(jù)綁定。
React是View層的框架,他負責將數(shù)據(jù)翻譯成Dom;
Redux是可預測的數(shù)據(jù)存儲層,他負責接收View 層發(fā)送過來的動作指令來進行數(shù)據(jù)的更新,當數(shù)據(jù)更新之后,React會負責將數(shù)據(jù)反應到頁面中。這里的 Action、State、Reducer 都是Redux中的概念,分別代表動作指令,數(shù)據(jù)存儲狀態(tài),對數(shù)據(jù)更新的代碼。
所以我們可以看到,用戶在點擊了 View 層之后,View層會觸發(fā)一些 Action 給Redux,Redux處理Action并更新數(shù)據(jù)之后,數(shù)據(jù)再被View層映射為Dom。圖片描述
既然已經(jīng)知道[ Redux + React ] 是什么了,那我們就要考慮一下為什么我們選擇[ Redux + React ],而不是選擇 jQuery,AngularJS 或者 Vue 等框架。
[ Redux + React ] 兩個非常核心的概念:
通過 [ Redux + React ] 來實現(xiàn)組件化;
實現(xiàn)單向數(shù)據(jù)流。
組件化方面,因為在 GrowingIO我們做的是一個企業(yè)級的應用,所以我們非常需要組件化的功能來將我們的業(yè)務模塊化。所以在這一點上React很好地滿足我們的需求,因為它有很豐富的組件庫,也有Facebook的支持,而 jQuery就不在考慮范圍之內(nèi)了。
單向數(shù)據(jù)流方面,在我們使用的過程中,我們意識到單向數(shù)據(jù)流能讓我們很清晰地把握系統(tǒng)中的數(shù)據(jù)流向,而不會產(chǎn)生太多的副作用。相較于雙向綁定,由于數(shù)據(jù)的更改是由框架自動完成的,可能更多時候我們沒有辦法非常有信心的把控數(shù)據(jù)的更改。所以最終我們選擇了[ Redux + React ]來完成我們的業(yè)務。AngularJs和Vue也就沒再考慮。
圖片描述
既然決定開始使用[ Redux + React ],我們就在互聯(lián)網(wǎng)上搜集了一些[ Redux + React ]最佳實踐,在所謂的最佳實踐中他們提到,要將所有的數(shù)據(jù)放在 Redux 層中,View層只做渲染和Action的調用。因為之前對單向數(shù)據(jù)流沒有太多的經(jīng)驗,對 Redux 也只是 API 層面的了解,所以我們就按照這套最佳實踐開始開發(fā)了。
圖片描述
整個開發(fā)過程還是比較順利的,上圖是我們開發(fā)出來的網(wǎng)站。因為這個過程中大家對數(shù)據(jù)放在哪,數(shù)據(jù)和頁面該怎么交互,都非常清晰,所以開發(fā)過程還算比較順利。但漸漸的當我們的網(wǎng)站越來越大的時候,我們遇到了一些性能和組件復用的問題。
圖片描述
隨著網(wǎng)站越來越慢,性能優(yōu)化就被提上了日程。經(jīng)過我們的調研,發(fā)現(xiàn)上圖中所謂的最佳實踐存在以下三個問題:
由于我們將所有的數(shù)據(jù)都放在 Store 中,Store中的數(shù)據(jù)越來越大;
React層的渲染次數(shù)越來越多,React 純變成了傻瓜式的組件,完全根據(jù)外界數(shù)據(jù)做組件渲染,本身并沒有存儲任何狀態(tài);
由于將所有的狀態(tài)都存儲在Store中,那么任何一次用戶的交互,都會觸發(fā) Action的調用,從而使得Store中的數(shù)據(jù)被更新,一旦數(shù)據(jù)格式被更新之后,React的渲染次數(shù)也會變多;
這里還有一個副作用,這是由于將所有的狀態(tài)數(shù)據(jù)都存儲在 Store中,那么當一個組件被應用在其他頁面的時候,這個組件就不是內(nèi)聚的,它很難被很容易地應用其他頁面。
圖片描述
所以針對以上幾個問題我們提出了三大類的性能提升方案:
減少Store中的更新次數(shù)。比如我們可以將更少的數(shù)據(jù)放在Store中;
避免不必要的組件渲染。也就是說當數(shù)據(jù)被更新之后,如果相應的組件內(nèi)的數(shù)據(jù)沒有被更新,那么這個組件內(nèi)部就不要進行重復的計算;
提高數(shù)據(jù)層的計算效率。在進行數(shù)據(jù)層計算的時候,避免不必要的計算。
圖片描述
這里將Store中的數(shù)據(jù)遷移到React層,這樣可以充分利用React組件化的能力,使得不必要的數(shù)據(jù)不要再放在Store中。根據(jù)我們的經(jīng)驗,數(shù)據(jù)可以分為兩大類,一類是業(yè)務數(shù)據(jù),一類是UI數(shù)據(jù),這里我們建議將業(yè)務數(shù)據(jù)存儲在Store中,而UI數(shù)據(jù)如果可能的話可以直接跟組件寫在一起。
什么是UI數(shù)據(jù)呢?這里我們舉個例子,假如有一個過濾器控件,這個過濾器在編輯的過程中的臨時信息存儲在自己的組件內(nèi)部,像這種組件強相關的信息,不要再存儲在Store中。
圖片描述
這里也列出了我們自己的一些控件,比如時間控件,過濾器控件,圖表控件。在使用這個控件過程中,一些UI信息和臨時狀態(tài),就不要再放在Store中了,而是存儲為控件本身的狀態(tài)。這樣整個頁面的交互,會使得Store的更新次數(shù)大大減少。
圖片描述
還有一種想法是假如Store 中的數(shù)據(jù)更新了,這塊數(shù)據(jù)會輻射到很多組件,但如果某一個組件內(nèi)部所需要的數(shù)據(jù)沒有被更新,那么我們是否可以避免這個組件不必要的計算。
在繼續(xù)往下探討之前,我們先來了解一下React的一個組件,有哪些關鍵的節(jié)點來判斷是否進行下一步的數(shù)據(jù)計算,大家可以看上圖。
shouldComponnetUpda<愛尬聊_百科詞條>te:當新的數(shù)據(jù)進來之后,React組件會調用一個函數(shù)。shouldComponentUpdate來判斷是否進行下一步的渲染,所以我們可以在這個函數(shù)中做一些基本的判斷,來決定是否要進行下一步的計算;
render:render函數(shù)是最核心的將子組件和數(shù)據(jù)組裝在一起的關鍵函數(shù),由這個函數(shù)來生成Virtual Dom;
Virtual Dom:Virtual Dom在被生成之后,會和上一次Virtual Dom進行比較,從而發(fā)現(xiàn)需要更新的瀏覽器Dom,再進行瀏覽器Dom的更新。
在了解了以上的React組件判斷是否進行下一步的計算的關鍵節(jié)點之后,我們就可以針對這幾個關鍵字點做優(yōu)化。
我們先從shouldComponentUpdate這個關鍵函數(shù)入手,在這個函數(shù)中我們判斷數(shù)據(jù)是否發(fā)生了變化,如果沒有發(fā)生變化我們就不更新。這里有兩種判斷思路,一種是使用深度比較,另外一種是使用淺度比較。
深度比較很容易懂,就是對數(shù)據(jù)建行非常細致的比較,但這樣比較的本身就比較耗費性能;
另外一種是淺度比較,這種比較方法只需要比較數(shù)據(jù)的引用是否發(fā)生變化即可,這種比較方法效率高但對于數(shù)據(jù)的更新和類型有一些要求。
這種數(shù)據(jù)結構需要滿足一旦數(shù)據(jù)被更新了,那么它的引用也隨之更新;而數(shù)據(jù)沒更新,那么它的引用也不發(fā)生變化。這讓我想到 Facebook 的ImmutableJS。
這里最直接的想法,就是我們在數(shù)據(jù)層直接使用 ImmutableJs,來達到上面我們提到的數(shù)據(jù)更新的效果。我們針對這種數(shù)據(jù)更新做了一些實踐,以及替代的解決方案,在下一頁PPT里面,我會進行相應的介紹。
在討論數(shù)據(jù)的管理策略之前,我們這里再回顧一下 [ Redux + React ] 的整個數(shù)據(jù)流。當 Reducer 更新完數(shù)據(jù)之后,它是如何判斷這些數(shù)據(jù)是否需要更新React層呢?這里上圖中被放大的一層 [React-Redux],它負責比較本次的狀態(tài)和上次狀態(tài)的引用是否發(fā)生變化,只有引用發(fā)生了變化,我們才進行下一步的React的渲染。
所以 [ Redux + React ] 這個框架本身就期望我們在數(shù)據(jù)層即 Store 中,能夠在更新數(shù)據(jù)之后,將引用同步更改。所以下面我們看看如何才能在數(shù)據(jù)層正確的更新數(shù)據(jù)以達到我們想要的效果:
第一種比較原始的辦法,就是在更新數(shù)據(jù)之前,將整個對象進行拷貝,然后更改對象的內(nèi)容,這樣能夠保證對象的引用發(fā)生了變化。然而深度拷貝是非常昂貴的,這是我們之前數(shù)據(jù)層計算效率降低的一個很重要的原因,因為一開始的時候數(shù)據(jù)量很小,我們沒有意識到這個問題的嚴重性。而且又是在創(chuàng)業(yè)公司,短平快的出產(chǎn)品,快速的驗證市場,才是那會兒公司的當務之急,所以在那個階段我們沒有過多的考慮。
第二個辦法就是我們希望能夠達到 ImmutableJs的效果,我們只更改需要被更新的數(shù)據(jù)引用。一種辦法是直接使用 ImmutableJs;另外一種是自己手動做這件事情,但這樣太麻煩,幸運的是 React 給我們提供了一個 react-addon-update 插件,可以很容易的實現(xiàn)對原生 Object 進行操作并達到我們想要的 ImmutableJs 的效果。
下面我就分別簡單介紹一下 ImmutableJs 和 react-addon-update 是如何幫助我們達到這種數(shù)據(jù)更新效果的。
ImmutableJs是一套Facebook提供的數(shù)據(jù)結構,這套數(shù)據(jù)結構的數(shù)據(jù)無法被直接修改,數(shù)據(jù)一旦被修改它的引用也會相應發(fā)生變化。他內(nèi)部使用了非常高效的算法能夠復用很多數(shù)據(jù),所以對ImmutableJs的數(shù)據(jù)更改非常高效。
圖片描述
大家可以在上圖中看到,圖里面的數(shù)據(jù)已經(jīng)變成ImmutableJs了。這樣對它的更新,都會將他的引用發(fā)生變化,看起來似乎我們的問題解決了。但其實并不然,ImmutableJS還是有很多限制的,如果你的項目環(huán)境不能滿足這些限制,也會帶來相應的問題。
一個就是因為他是一套新的數(shù)據(jù)結構,所以在操作的這套數(shù)據(jù)結構這個時候你不能用原生的JS API,需要去查詢它的API來完成操作,但用它的API很多,很雜,而JS語言本身沒有靜態(tài)類型檢查,在使用時候非常容易出錯,所以這里建議可能需要配合Typescript等支持語法檢查的語言一起使用,這樣能夠編譯階段檢查出來問題;
第二個問題跟第一個問題可能有一點相似,就是ImmutableJS所生成的對象和原生的對象在寫代碼的過程中很難區(qū)分,這里同樣建議使用Typescript來配合;
第三個就是使用 ImmuableJS生成的數(shù)據(jù)結構,無法使用現(xiàn)有的各種JS庫來操作,這會帶來很大的不方便。
針對以上的這個問題我總結一下可能的解決方案,第一個是配合Typescript等有靜態(tài)類型檢查的語言一起使用,第二個是約定命名變量規(guī)則,第三個是只在某個模塊內(nèi)部使用ImmutableJS,對外依然使用 Plain Object。
在GrowingIO 內(nèi)部,因為一開始我們用的是ES6,所以我們并沒有大規(guī)模使用ImmutableJS,只是在某些模塊內(nèi)部使用了ImmutableJS,比如某些數(shù)據(jù)量比較大的Reducer。
那怎么樣才能在這個ImmutableJS和克數(shù)據(jù)之間找到一種中間方案呢。這里我要介紹一下 react-addon-update插件。這個插件結束兩參數(shù),一個是要更新的對象,另外一個我們叫做 spec,代指你告訴這個函數(shù)該如何更新對象。
使用這個插件來替代ImmutableJS的好處很明顯就是這個插件操作的是Plain Object,雖然在更新操作上會稍微慢一點點,但是由于他們的數(shù)據(jù)結構沒有發(fā)生任何變化,這使得實際代碼過程中的數(shù)據(jù)一直都是Plain Object。
同時也沒達到我們想要的 ImmutableJS的效果,所以如果一些同學像我們一樣,代碼已經(jīng)在使用ES6編寫了,可以嘗試下這個插件。這樣你在更新數(shù)據(jù)的時候就可以避免克隆操作,同時又能夠達到ImmutableJS數(shù)據(jù)結構的效果,真是一舉兩得啊。
圖片描述
所以這里我們總結一下,在使用[ Redux + React ]的過程中,我們想要提高他的性能的話,可以從這三方面入手:
減少Store的更新次數(shù),這個主要通過組件化來解決,UI數(shù)據(jù)和臨時數(shù)據(jù)不要再存放在Store中;
避免不必要的 Component渲染,假設數(shù)據(jù)層能夠提供準確的數(shù)據(jù)更新,即數(shù)據(jù)更新了,數(shù)據(jù)引用也會發(fā)生變化,數(shù)據(jù)沒有更新,那么它的引用就不發(fā)生變化,這樣的話我們可以在shouldComponentUpdate 函數(shù)中進行智能的判斷師傅要進行進一步的計算;
第三步提高數(shù)據(jù)層的計算效率,通過使用 ImmutableJS 或者 react-addon-update 來達到只更新想要更新的數(shù)據(jù)。
Q&A
Q1:redux+react相比較vue,angluar有何優(yōu)勢?
A1:再進行選型的時候,我主要考慮幾個點,一個是社區(qū)是否夠強大,另外一個是當工程量比較大的時候,這個框架是否能夠滿足工程的需要。
社區(qū)方面:react由Facebook支持,并且已經(jīng)有阿里等國內(nèi)大公司也在支持,vue稍微偏小眾,angular過于封閉了。
工程方面:redux+react單向數(shù)據(jù)流,在企業(yè)級應用會使得工程難度降低很多相對于 vue 和 angular 的雙向數(shù)據(jù)流。
所以雖然 redux + react 咋開發(fā)小程序的時候,不如 vue 和 angular 快,但對于復雜網(wǎng)頁的企業(yè)級服務應用來說,還是非常適合的。
Q2:redux更大的挑戰(zhàn)來自如何設計action,設計state樹形結構,有何經(jīng)驗分享?
A2:我傾向于將 redux 中的 state 分為兩類,一類是后端數(shù)據(jù)緩存,跟后端的數(shù)據(jù)庫中的概念進行映射,這種的多帶帶做 reducer;另一類是前端頁面相關的信息多帶帶做一個 reducer。這樣可以使得操作數(shù)據(jù)非常快。而且引用React 去引用數(shù)據(jù)也清晰。
action 命名很重要,需要有嚴格的 Namespace 的概念。比如操作數(shù)據(jù)緩存類的 reducer 以 CONCEPT_ 開頭,操作頁面數(shù)據(jù)相關的以 PAGE_ 開頭。可以根據(jù)業(yè)務場景進行合適的命名空間的分區(qū)。
Q3:關于UI數(shù)據(jù),比如剛剛講到的日歷,如果不放在store里,那點擊后的時間怎么告訴其它組件渲染對應的數(shù)據(jù)呢?
A3:日歷這種控件,在選擇過程中的臨時數(shù)據(jù)存儲為 component 的state,直到用戶點擊確認按鈕,才調用 action,將最終的時間同步到 store 中。
Q4:redux是典型的函數(shù)式編程思想,什么樣的技術團隊適合使用?
A4:事實上,寫 React 和寫 Java 非常像,Redux + React 單向數(shù)據(jù)流使得大項目的工程難度降低非常多,很多 Junior 的工程師,只要代碼寫得還不錯,稍微學習一下就可以很快上手。當然要求工程師對 HTML && CSS 比較熟悉,才能把界面畫好。
Q5: 關于redux性能優(yōu)化問題,有使用reselect插件嗎?
A5:使用了。reselect 和 redux-react 默認同樣都是基于引用進行比較的。所以大家在操作 reducer 中的數(shù)據(jù)的時候,要保證引用隨著數(shù)據(jù)同時被更新,不需要更新的數(shù)據(jù)引用也不要更新。比如使用 react-addon-update 或者 ImmutableJs 來加速這種數(shù)據(jù)效果的更新。
Q6: 關于deepclone? Object.assign 效率低嗎?
A6:Object.assign 效率不低,屬于shallowClone,但是如果對象數(shù)據(jù)有多層的話,就需要手動去做這件事情,才能保證上面提到的引用數(shù)據(jù)效果。使用 react-addon-update 或者 ImmutableJs 的話,代碼看起來會比較簡潔。