摘要:數據管理及性能優化統一管理數據這一部份算是重頭戲吧。重復渲染導致卡頓這套的東西在家校群頁面上用得很歡樂,以至于不用怎么寫都沒遇到過什么性能問題。但放到移動端上,我們在列表頁重構的時候就馬上遇到卡頓的問題了。列表頁目前的處理辦法是將值換成。
本文來自于騰訊bugly開發者社區,非經作者同意,請勿轉載,原文地址:http://dev.qq.com/topic/57908...
最近一個季度,我們都在為手Q家校群做重構優化,將原有那套問題不斷的框架換掉。經過一些斟酌,決定使用react 進行重構。選擇react,其實也主要是因為它具有下面的三大特性。
React的特性學習React的好處就是,學了一遍之后,能夠寫web, node直出,以及native,能夠適應各種紛繁復雜的業務。需要輕量快捷的,直接可以用Reactjs;需要提升首屏時間的,可以結合React Server Render;需要更好的性能的,可以上React Native。
但是,這其實暗示學習的曲線非常陡峭。單單是Webpack+ React + Redux就已夠一個入門者夠嗆,更何況還要兼顧直出和手機客戶端。不是一般人能hold住所有端。
Virtual Dom(下稱vd)算是React的一個重大的特色,因為Facebook宣稱由于vd的幫助,React能夠達到很好的性能。是的,Facebook說的沒錯,但只說了一半,它說漏的一半是:“除非你能正確的采用一系列優化手段”。
另一個被大家所推崇的React優勢在于,它能令到你的代碼組織更清晰,維護起來更容易。我們在寫的時候也有同感,但那是直到我們踩了一些坑,并且漸漸熟悉React+ Redux所推崇的那套代碼組織規范之后。
那么?
上面的描述不免有些先揚后抑的感覺,那是因為往往作為React的剛入門者,都會像我們初入的時候一樣,對React滿懷希望,指意它幫我們做好一切,但隨著了解的深入,發現需要做一些額外的事情來達到我們的期待。
對React的期待初學者對React可能滿懷期待,覺得React可能完爆其它一切框架,甚至不切實際地認為React可能連原生的渲染都能完爆——對框架的狂熱確實會出現這樣的不切實際的期待。讓我們來看看React的官方是怎么說的。React官方文檔在Advanced Performanec這一節,這樣寫道:
One of the first questions people ask when considering React for a project is whether their application will be as fast and responsive as an equivalent non-React version
顯然React自己也其實只是想盡量達到跟非React版本相若的性能。React在減少重復渲染方面確實是有一套獨特的處理辦法,那就是vd,但顯示在首次渲染的時候React絕無可能超越原生的速度,或者一定能將其它的框架比下去。因此,我們在做優化的時候,可的期待的東西有:
首屏時間可能會比較原生的慢一些,但可以嘗試用React Server Render (又稱Isomorphic)去提高效率
用戶進行交互的時候,有可能會比原生的響應快一些,前提是你做了一些優化避免了浪費性能的重復渲染。
以手Q家校群功能頁React重構優化為例手Q家校群功能頁主要由三個頁面構成,分別是列表頁、布置頁和詳情頁。列表頁已經重構完成并已發布,布置頁已重構完畢準備提測,詳情頁正在重構。與此同時我們已完成對列表頁的同構直出優化,并已正在做React Native優化的鋪墊。
這三個頁面的重構其實覆蓋了不少頁面的案例,所以還是蠻有代表性的,我們會將重構之中遇到的一些經驗穿插在文章里論述。
在手Q家校群重構之前,其實我們已經做了一版PC家校群。當時將native的頁面全部web化,直接就采用了React比較常用的全家桶套裝:
構建工具 => gulp + webpack
開發效率提升 => redux-dev-tools + hot-reload
統一數據管理=> redux
性能提升 => immutable + purerender
路由控制器 => react-router(手Q暫時沒采用)
為什么我們在優化的時候主要講手Q呢?畢竟PC的性能在大部份情況下已經很好,在PC上一些存在的問題都被PC良好的性能掩蓋下去。手機的性能不如PC,因此有更多有價值的東西深挖。開發的時候我就跟同事開玩笑說:“沒做過手機web優化的都真不好意思說自己做過性能優化啊“。
構建針對React做的優化我在《性能優化三部曲之一——構建篇》提出,“通過構建,我們可以達成開發效率的提升,以及對項目最基本的優化”。在進行React重構優化的過程中,構建對項目的優化作用必不可少。在本文暫時不贅述,我另外開辟了一篇《webpack使用優化(react篇)》進行具體論述。
開發效率提升工具在PC端使用Redux的時候,我們都很喜歡使用Redux-Devtools來查看Redux觸發的action,以及對應的數據變化。PC端使用的時候,我們習慣擺在右邊。但移動端的屏幕較少,因此家校群項目使用的時候放在底部,而且由于性能問題,我們在constant里設一個debug參數,然后在chrome調試時打開,移動端非必須的時候關閉。否則,它會導致移動web的渲染比較低下。
數據管理及性能優化 Redux統一管理數據這一部份算是重頭戲吧。React作為View層的框架,已經通過vd幫助我們解決重復渲染的問題。但vd是通過看數據的前后差異去判斷是否要重復渲染的,但React并沒有幫助我們去做這層比較。因此我們需要使用一整套數據管理工具及對應的優化方法去達成。在這方法,我們選擇了Redux。
Redux整個數據流大體可以用下圖來描述:
Redux這個框架的好處在于能夠統一在自己定義的reducer函數里面去進行數據處理,在View層中只需要通過事件去處觸發一些action就可以改變地應的數據,這樣能夠使數據處理和dom渲染更好地分離,而避免手動地去設置state。
在重構的時候,我們傾向于將功能類似的數據歸類到一起,并建立對應的reducer文件對數據進行處理。如下圖,是手Q家校群布置頁的數據結構。有些大型的SPA項目可能會將初始數據分開在不同的reducer文件里,但這里我們傾向于歸到一個store文件,這樣能夠清晰地知道整個文件的數據結構,也符合Redux想統一管理數據的想法。然后數據的每個層級與reducer文件都是一一對應的關系。
重復渲染導致卡頓這套React + Redux的東西在PC家校群頁面上用得很歡樂, 以至于不用怎么寫shouldComponentUpdate都沒遇到過什么性能問題。但放到移動端上,我們在列表頁重構的時候就馬上遇到卡頓的問題了。
什么原因呢?是重復渲染導致的!!!!!!
說好的React vd可以減少重復渲染呢?!!!
請別忘記前提條件!!!!
你可以在每個component的render里,放一個console.log("xxx component")。然后觸發一個action,在優化之前,幾乎全部的component都打出這個log,表明都重復渲染了。
更正:可見后面yeatszhang同學的解釋。
(網圖,引用的文章太多以致于不知道哪篇才是出處)
上圖是React的生命周期,還沒熟悉的同學可以去熟悉一下。因為其中的shouldComponentUpdate是優化的關鍵。React的重復渲染優化的核心其實就是在shouldComponentUpdate里面做數據比較。在優化之前,shouldComponentUpdate是默認返回true的,這導致任何時候觸發任何的數據變化都會使component重新渲染。這必然會導致資源的浪費和性能的低下——你可能會感覺比較原生的響應更慢。
這時你開始懷疑這世界——是不是Facebook在騙我。
當時遇到這個問題我的開始翻閱文檔,也是在Facebook的Advanced Performance一節中找到答案:Immutablejs。這個框架已被吹了有一年多了吧,吹這些框架的人理解它的原理,但不一定實踐過——因為作為一線移動端開發者,打開它的github主頁看dist文件,50kb,我就已經打退堂鼓了。只是遇到了性能問題,我們才再認真地去了解一遍。
Immutable這個的意思就是不可變,Immutablejs就是一個生成數據不可變的框架。一開始你并不理解不可變有什么用。最開始的時候Immutable這種數據結構是為了解決數據鎖的問題,而對于js,就可以借用來解決前后數據比較的問題——因為同時Immutablejs還提供了很好的數據比較方法——Immutable.is()。小結一下就是:
Immutablejs本身就能生成不可變數據,這樣就不需要開發者自己去做數據深拷貝,可以直接拿prevProps/prevState和nextProps/nextState來比較。
Immutable本身還提供了數據的比較方法,這樣開發者也不用自己去寫數據深比較的方法。
說到這里,已萬事俱備了。那東風呢?我們還欠的東風就是應該在哪里寫這個比較。答案就是shouldComponentUpdate。這個生命周期會傳入nextProps和nextState,可以跟component當前的props和state直接比較。這個就可以參考pure-render的做法,去重寫shouldComponentUpdate,在里面寫數據比較的邏輯。
其中一位同事polarjiang利用Immutablejs的is方法,參考pure-render-decorator寫了一個immutable-pure-render-decorator。
那具體怎么使用immutable + pure-render呢?
對于immutable,我們需要改寫一下reducer functions里面的處理邏輯,一律換成Immutable的api。
至于pure-render,若是es5寫法,可以用使mixin;若是es6/es7寫法,需要使用decorator,在js的babel loader里面,新增plugins: [‘transform-decorators-legacy’]。其es6的寫法是:
@pureRender export default class List extends Component { ... }Immutablejs帶來的一些問題
不重新渲染
你可能會想到Immutable能減少無謂的重新渲染,但可能沒想過會導致頁面不能正確地重新渲染。目前列表頁在老師進入的時候是有2個tab的,tab的切換會讓列表也切換。目前手Q的列表頁學習PC的列表頁,兩個列表共用一套dom結構(因為除了作業布置者名字之外,兩個列表一模一樣)。上了Immutablejs之后,當碰巧“我發布的“列表和”全部“列表開頭的幾個作業都是同一個人布置的時候,列表切換就不重新渲染了。
引入immutable和pureRender后,render里的JSX注意一定不要有同樣的key(如兩個列表,有重復的數據,此時以數據id來作為key就不太合適,應該要用數據id + 列表類型作為key),會造成不渲染新數據情況。列表頁目前的處理辦法是將key值換成id + listType。
(列表頁兩個列表的切換)
這樣寫除了保證在父元素那一層知曉數據(key值)不同需要重新渲染之外,也保證了React底層渲染知道這是兩組不同的數據。在React源文件里有一個ReactChildReconciler.js主要是寫children的渲染邏輯。其中的updateChildren里面有具體如何比較前后children,然后再決定是否要重新渲染。在比較的時候它調用了shouldUpdateReactComponent方法。我們看到它有對key值做比較。在兩個列表中有不同的key,在數據相似的情況下,能保證兩者切換的時候能重新渲染。
function shouldUpdateReactComponent(prevElement, nextElement) { var prevEmpty = prevElement === null || prevElement === false; var nextEmpty = nextElement === null || nextElement === false; if (prevEmpty || nextEmpty) { return prevEmpty === nextEmpty; } var prevType = typeof prevElement; var nextType = typeof nextElement; if (prevType === "string" || prevType === "number") { return nextType === "string" || nextType === "number"; } else { return nextType === "object" && prevElement.type === nextElement.type && prevElement.key === nextElement.key; } }
Immutablejs太大了
上文也提到Immutablejs編譯后的包也有50kb。對于PC端來說可能無所謂,網速足夠快,但對于移動端來說壓力就大了。有人寫了個seamless-immutable,算是簡易版的Immutablejs,只有2kb,只支持Object和Array。
但其實數據比較邏輯寫起來也并不難,因此再去review代碼的時候,我決定嘗試自己寫一個,也是這個決定讓我發現了更多的奧秘。
針對React的這個數據比較的深比較deepCompare,要點有2個:
盡量使傳入的數據扁平化一點
比較的時候做一些限制,避免溢出棧
先上一下列表頁的代碼,如下圖。這里當時是學習了PC家校群的做法,將component作為props傳入。這里的則是列表頁的渲染,
針對deepCompare的第1個要點,扁平化數據,我們很明顯就能定位出其中一個問題了。例如
props.hw = { listMine: [ {...}, {...}, ... ], listAll: [ {...}, {...}, ... ], }
但如果我們提前在傳入之前判斷當前在哪個列表,然后傳入對應列表的數量,則會像這樣:
props.hw = 20;
兩者比較起來,顯示是后者簡單得多。
針對deepCompare第2點,限制比較的條件。首先讓我們想到的是比較的深度。一般而言,對于Object和Array數據,我們都需要遞歸去進行比較,出于性能的考慮,我們都會限制比較的深度。
除此之外,我們回顧一下上面的代碼,我們將幾個React component作為props傳進去了,這會在shouldComponentUpdate里面顯示出來。這些component的結構大概如下:
$$typeof // 類型 _owner // 父組件 _self: // 僅開發模式出現 _source: // 僅開發模式出現 _store // 僅開發模式出現 key // 組件的key屬性值 props // 從傳入的props ref // 組件的ref屬性值 type 本組件ReactComponent
因此,針對component的比較,有一些是可以忽略的,例如$$typeof, _store, _self, _source, _owner。type這個比較復雜,可以比較,但僅限于我們定好的比較深度。如果不做這些忽略,這個深比較將會比較消耗性能。關于這個deepCompare的代碼,我放在了pure-render-deepCompare-decorator。
不過其實,將component當作props傳入更為靈活,而且能夠增加組件的復用性,但從上面看來,是比較消耗性能的。看了官方文檔之后,我們嘗試換種寫法,主要就是采用的做法,然后用this.props.children在
本以為React可能會對children這個props有什么特殊處理,但它依然是將children當作props,傳入shouldComponentUpdate,這就迫使父元素再去判斷是否進一步進行渲染。
那自己再去判斷。這樣我們對pure-render-deepCompare-decorator要進行一些修改,當輪到props.children判斷的時候,我們要求父元素直接重新渲染,這樣就能交給子元素去做下一步的處理。
如果還好,如果還有像
lodash.merge可以解決大部份場景
(此段更新于2016年6月30日)由于immutable的大小問題一直縈繞頭上,久久不得散去,因此再去找尋其它的方案。后面決定嘗試一下lodash.merge,并用上之前自己寫的pureRender。在渲染性能上還可以接受,在僅比immutable差一點點(后面會披露具體數據),但卻帶來了30kb的減包。
性能優化小Tips這里歸納了一些其它性能優化的小Tips。
既然將數據主要交給了Redux來管理,那就盡量使用Redux管理你的數據和狀態state,除了少數情況外,別忘了shouldComponentUpdate也需要比較state。
Component的render里不動態bind方法,方法都在constructor里bind好,如果要動態傳參,方法可使用閉包返回一個最終可執行函數。如:showDelBtn(item) { return (e) => {}; }。如果每次都在render里面的jsx去bind這個方法,每次都要綁定會消耗性能。
傳得太多,或者層次傳得太深,都會加重shouldComponentUpdate里面的數據比較負擔,因此,也請慎用spread attributes()。
這個用法是工業聚在React討論微信群里教會的,我們可以將不怎么變動,或者不需要傳入狀態的component寫成const element的形式,這樣能加快這個element的初始渲染速度。
路由控制與拆包當項目變得更大規模與復雜的時候,我們需要設計成SPA,這時路由管理就非常重要了,這使特定url參數能夠對應一個頁面。
PC家校群整個設計是一個中型的SPA,當js bundle太大的時候,需要拆分成幾個小的bundle,進行異步加載。這時可以用到webpack的異步加載打包功能,require。
在重構手Q家校群布置頁的時候,我們有不少的浮層,列表有布置頁內容主浮層、同步到多群浮層、科目管理浮層以及指定群成員浮層。這些完全可以使用react-router進行管理。但是由于當時一早使用了Immutablejs,js bundle已經比較大,我們就不打算使用react-router了。但后面仍然發現包比重構前要大一些,因此為了保證首屏時間不慢于重構前,我們希望在不用react-router的情況下進行分包,其實也并不難,如下面2幅圖:
首先在切換浮層方法里面,使用require.ensure,指定要加載哪個包。
在setComponent方法里,將component存在state里面。
在父元素的渲染方法里,當state有值的時候,就會自動渲染加載回來的component。
目前只有列表頁發布外網了,我們比較了優化前后的首屏可交互時間,分別有18%和5.3%的提升。
渲染FPS更新于2016年7月2日
Android
React重構后第一版,當時還沒做任何的優化,發現平均FPS只有22(雖然Android的肉眼感受不出來),而后面使用Immutable或者Lodash.merge都非常接近,能達到42或以上。而手機QQ可接受的FPS最少值是30FPS。因此使用Immutable和Lodash.merge的優化還是相當明顯的。
重構后第一版
Immutable
Lodash.merge
iOS
在iOS上的fps差距尤為明顯。重構后第一版,拉了大概5屏之后,肉眼會有卡頓的感覺,拉到了10屏之后,數據開始掉到了20多30。而Immutable和Lodash.merge則大部份時間保持在50fps以上,很多時候還能達到非常流暢的60fps。
重構后第一版
Immutable
Lodash.merge
Chrome模擬器
用Chrome模擬器也能看出一些端倪。在Scripting方面,Immutable和Lodash.merge的耗時是最少的,約700多ms,而重構后的第一版則需要1220ms。Lodash.merge在rendering和painting上則沒占到優勢,但Immutable則要比其它兩個要少30% - 40%。由于測試的時候是在PC端,PC端的性能又極好,所以不管是肉眼,還是數據,對于不是很復雜的需求,總體的渲染性能看不出非常明顯的差距。
重構后第一版
Immutable
Lodash.merge
從上面的數據看來,在移動端使用Immutable和Lodash.merge相對于不用,會有較大的性能優勢,但Immutable相對于Lodash.merge在我們需求情景下暫時沒看出明顯的優勢,筆者估計可能是由于項目數據規模不大,結構不復雜,因此Immutable的算法優勢并沒有充分發揮出來。
測試注明
Android端測試FPS是使用了騰訊開發的GT隨身調。而iOS則使用了Macbook里xCode自帶的instrument中的animation功能。Chrome模擬器則使用了Chrome的timeline。測試的方式是勻速滾動列表,拉出數據進行渲染。
React性能優化軍規我們在開發的過程中,將上面所論述的內容,總結成一個基本的軍規,銘記于心,就可以保證React應用的性能不至于太差。
渲染相關提升級項目性能,請使用immutable(props、state、store)
請pure-render-decorator與immutablejs搭配使用
請慎用setState,因其容易導致重新渲染
謹慎將component當作props傳入
請將方法的bind一律置于constructor
請只傳遞component需要的props,避免其它props變化導致重新渲染- - (慎用spread attributes)
請在你希望發生重新渲染的dom上設置可被react識別的同級唯一key,否則react在某些情況可能不會重新渲染。
請盡量使用const element
tap事件簡單的tap事件,請使用react-tap-event-plugin 開發環境時,最好引入webpack的環境變量(僅在開發環境中初始化),在container中初始化。生產環境的時候,請將plugin跟react打包到一起(需要打包在一起才能正常使用,因為plugin對react有好多依賴),外鏈引入。
目前參考了這個項目的打包方案:https://github.com/hartmamt/r...
Facebook官方issue: https://github.com/facebook/r...
React-tap-event-plugin github: https://github.com/zilverline...
復雜的tap事件,建議使用tap component 家校群列表頁的每個作業的tap交互都比較復雜,出了普通的tap之外,還需要long tap和swipe。因此我們只好自己封裝了一個tap component
Debug相關移動端請慎用redux-devtools,易造成卡頓
Webpack慎用devtools的inline-source-map模式 使用此模式會內聯一大段便于定位bug的字符串,查錯時可以開啟,不是查錯時建議關閉,否則開發時加載的包會非常大。
其它
慎用太新的es6語法。 Object.assign等較新的類庫避免在移動端上使用,會報錯。 Object.assign目前使用object-assign包。或者使用babel-plugin-transform-object-assign插件。會轉換成一個extends的函數:
var _extends = ...; _extends(a, b);
注意Object.assign是淺拷貝 Object.assign是淺拷貝,若數據結構層次較深的時候會拷貝失敗,直接回傳原本的object reference,此處推薦lodash.merge。
如有錯誤,請斧正!
PS: 要看效果得將一個QQ群組轉換成家校群,可到此網址進行轉換(手Q/PC都可以訪問):http://qun.qq.com/homework/。
轉換之后,可以通過QQ群的加號面板,或者群資料卡進入。
更多精彩內容歡迎關注bugly的微信公眾賬號:
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/86402.html
摘要:更多資源請文章轉自月份前端資源分享的作用數組元素隨機化排序算法實現學習筆記數組隨機排序個變態題解析上個變態題解析下中的數字前端開發筆記本過目不忘正則表達式聊一聊前端存儲那些事兒一鍵分享到各種寫給剛入門的前端工程師的前后端交互指南物聯網世界的 更多資源請Star:https://github.com/maidishike... 文章轉自:https://github.com/jsfr...
閱讀 3034·2023-04-26 03:01
閱讀 3538·2023-04-25 19:54
閱讀 1592·2021-11-24 09:39
閱讀 1374·2021-11-19 09:40
閱讀 4250·2021-10-14 09:43
閱讀 2062·2019-08-30 15:56
閱讀 1490·2019-08-30 13:52
閱讀 1660·2019-08-29 13:05