記錄一次利用 Timeline/Performance 工具進行 React 效能優化的真實案例

發表於2017-07-10

記錄一次利用  Timeline/Performance 工具進行 React 效能優化的真實案例

   設計圖鑑賞推薦:別人家的設計作品

效能優化可以說是衡量一個react程式設計師的水平重要標準。

在學習react之初的時候,由於對react不夠了解,因此寫的專案雖然功能都實現了,但是效能優化方面的考慮卻做得很少,因此回過頭髮現以前自己以前寫的react程式碼確實有點糟糕。

為了提高自己的react水平,閒暇之餘就把以前的老專案拿出來分析優化,看看都有哪些問題,以及如何優化。這裡就以我以前做過的一個《投資日曆》為例做一次優化記錄。

專案線上地址:https://www.itiger.com/activity/forapp/finance-calendar

優化工具timeline/performance基礎使用教程:
https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/timeline-tool?hl=zh-cn

chrome在版本57還是58的時候,將Timeline更名為performance

該專案主要的主要難點與效能瓶頸在於日曆的左右滑動與切換。由於需求定製程度非常高,沒有合適的第三方日曆外掛,所以就自己實現了一個。支援週日歷與月日曆的切換,支援左右滑動切換日期。

滑動效果僅支援移動端

問題出現在公司一款老的android測試機,發現動畫效果非常卡頓。因此有了優化的必要。

利用工具定位問題

首先利用performance工具的的錄製功能錄製一段操作過程。
點選左上角的黑色原點開始錄製。錄製過程中,多次滑動週日歷即可。然後大約5~10秒點選stop按鈕停止錄製。

錄製結果如圖。

記錄一次利用  Timeline/Performance 工具進行 React 效能優化的真實案例

發現很多紅幀,以及不正常的記憶體佔用

從上圖中我們可以發現以下問題:

1、 窗格中出現了紅幀。出現紅幀表示頁面已經超負荷,會出現卡頓,響應緩慢等現象。
2、 大量的黃色區域,黃色區域越大,表示JavaScript的執行過程中的壓力也越大。
3、 高額的記憶體佔用,以及不正常的波動曲線(藍色)。詳細資訊可以在上圖中的JS Heap中檢視。26.6 ~ 71.6M

記錄一次利用  Timeline/Performance 工具進行 React 效能優化的真實案例

窗格圖

我們可以在Main中觀察到當前時刻的函式呼叫棧詳情。當出現紅幀,選中紅幀區域,Main區域發現變化,變為當前選擇時段的函式呼叫棧詳情。我們會發現函式呼叫棧最上層有一個紅色三角形。點選會在下面的Summary裡發現對應的資訊以及警告。如下圖中的Warning: Recuring handler took 86.69 ms

記錄一次利用  Timeline/Performance 工具進行 React 效能優化的真實案例

找到一個紅點仔細觀察,發現一個警告

4、 層級很高的函式呼叫棧。檢視紅色區域的函式呼叫棧,我們會發現大量的react元件方法被重複呼叫。

記錄一次利用  Timeline/Performance 工具進行 React 效能優化的真實案例

一步一步開始優化

從上面的分析就可以簡單看出,雖然實現了非常複雜的功能,看上去很厲害的樣子,其實內部非常糟糕。幾乎可以作為react用法的反面教材了。

優化分析1

在上面的函式呼叫棧中,我們發現有一個方法出現的次數非常多,那就是receiveComponent。因此可以預想到某個元件裡肯定使用了receiveComponent相關的生命週期的方法。檢查程式碼,確實發現了幾處componentWillReceiveProps的使用。

剛開始學習react時可能會認為生命週期是一個學習難點,我們不知道什麼情況下去使用它們。慢慢的隨著經驗的增加,才發現,生命週期方法是萬萬不能輕易使用的。特別是與props/state改變,與元件重新渲染相關的幾個生命週期,如componentWillReceivePropsshouldComponentUpdatecomponentWillUpdate等。這個實際案例告訴我們,他們的使用,會造成高額的效能消耗。所以不到萬不得已,不要輕易使用他們。

曾經看到過一篇英文博文,分析的是寧願多幾次render,也不要使用shouldComponentUpdate來優化程式碼。但是文章地址找不到,如果有其他看過的朋友請在評論裡留言分享一下,感謝

而只有componentDidMount是非常常用的。

上面幾行簡單的程式碼,卻暴露了一個非常恐怖的問題。一個是使用了生命週期componentWillReceiveProps。而另一個則是在props改變的同時,還修改了元件的state。我們知道當props在父級被改變時會造成元件的重新渲染,而元件內部的state的改變同樣也會造成元件的重新渲染,因此這幾句簡單的程式碼,讓元件的渲染無形中發生了很多次。

因此優化的方向就朝這兩個方向努力。首先不能使用componentWillReceiveProps,其次我發現navProcess其實可以在父級元件中計算,並通過props傳遞下來。所以優化後的程式碼如下:

意外的驚喜是發現該元件最終優化成為了一個無狀態元件,輕裝上陣,完美。

這樣優化之後,重新渲染的發生少了好幾倍,執行壓力自然減少很多。因此當滑動週日歷時已經不會有紅幀發生了。但是月日曆由於DOM節點更多,仍然存在問題,因此核心的問題還不在這裡。我們還得繼續觀察。

優化分析2

在函式呼叫棧中我們可以很明顯的看到ani方法。而這個方法是我自己寫的運動實現。因此我得重點關注它的實現中是不是存在什麼問題。仔細瀏覽一遍,果然有問題。

發現在ani方法的回撥中,呼叫了2次setDate方法。

該setDate方法是在父級中定義用來修改父級state的方法。他的每一次呼叫都會引發由上自下的重新渲染,因此多次呼叫的代價是非常大的。所以我將要面臨的優化就是想辦法將這兩次呼叫合併為一次。

先看看優化以前setDate方法的定義是如何實現的。我想要通過不同的number來修改不同的state屬性。但是沒有考慮如果需要修改多個呢?

修改該方法為,傳遞一個物件字面量進去進行修改

該方法有兩處優化,第一處優化是傳入的引數調整,想要修改那一個就直接傳入,用法類似setState。第二處優化是在this.process方法中只呼叫一次this.setState,總之這樣處理的目的都是統一的,當想要資料修改時只發生一次渲染。而之前的方法會導致3次甚至多次渲染。這樣優化之後,效能自然會提升很多。

優化分析3

但是優化並沒有結束,因為再錄製一段檢視,仍然會發現紅幀出現。
進一步檢視Calendar元件,發現每一次滑動切換,都會發生4次渲染。肯定有問題。

我的目的是最多發生兩次無法避免的渲染。多餘的肯定是因為程式碼的問題導致的冗餘渲染。因此繼續檢視程式碼。

發現在遞迴呼叫ani方法時,this.timer並沒有被及時取消。

因此修改如下:

這樣優化之後,發現記憶體佔用下降一些,但是紅幀仍然存在。看來計算量並沒有下降。繼續優化。

優化分析4

發現Calendar元件中,根據props中的curDate,curMonth計算而來的weekInfo與monthInfo被寫在了該元件的state中。由於state中資料的變化都會導致重新渲染,而我發現在程式碼中有多處對他們進行修改。

其實這種根據props中的引數計算而來的資料是萬萬不能寫在state中的,因為props資料的變化也會導致元件重新整理重新渲染,因此一個資料變化就會導致不可控制的多次渲染。這個時候更好的方式是直接在render中計算。因此優化如下:

優化結果如下圖:

記錄一次利用  Timeline/Performance 工具進行 React 效能優化的真實案例

image.png

與第一張圖對比,我們發現,運動過程中出現的紅幀沒有了。二是窗格中黃色區域大量減少,表示js的計算量減少很多。三是記憶體佔用大幅降低,從最高的71M減少到了33M。記憶體的增長也更加平滑。

後續的優化大致目的都是一樣。不再贅述。

總結一下:

  1. 儘量避免生命週期方法的使用,特別是與狀態更新相關的生命週期,使用時一定要慎重。
  2. 能通過props重新渲染元件,就不要在額外新增state來增加渲染壓力。
  3. 一切的優化方向就是在實現功能的前提下減少重新渲染的發生。

這其中涉及到的技巧就需要大家在實戰中慢慢掌握了。

相關文章