如何利用工具提高React頁面渲染效能之Perf

塵境天藍發表於2016-12-21

前言

用 React 一段時間了,也做了不少列表頁。在用 React 做無限下拉載入的列表頁時發現個問題:頁面前幾頁渲染速度還挺快的,但是越往下拉載入內容頁面的渲染就越慢。這是怎麼回事呢?
讓我們先來看下 React 的元件渲染流程吧。

React 的元件渲染流程

React 的元件渲染分為初始化渲染和更新渲染。在初始化時,React 會呼叫根元件下所有元件的 render 方法進行渲染。

在每個生命週期更新時,React 會先呼叫 shouldComponentUpdate(nextProps, nextState) 方法來判斷該元件是否需要更新。該方法會返回 true 或 false 來表示更新或不需要更新。如果不需要更新,則直接保持不變;如果需要更新,則呼叫 render 方法生成新的虛擬 DOM,然後再用 diff 演算法與舊的虛擬 DOM 進行對比,如果結果一致就不更新;如果對比不同,則根據最小粒度改變去更新 DOM。
整個過程如下圖所示。
_2016_09_22_10_40_05

ShouldComponentUpdate 在預設情況下返回的是 true。也就是說 React 預設會呼叫所有元件的 render 方法生成虛擬 DOM,然後再與舊虛擬 DOM 比較以確定最終元件是否需要更新。這個 render 和 diff 對比的過程對於只是兄弟元件發生了改變,而本身並沒有變化的元件來說,很明視訊記憶體在資源浪費。

那麼如何能直觀的知道這些浪費都發生在哪些過程中呢?這就輪到 Perf 出場了。

接下來讓我們先來了解下什麼是 Perf,再看看它都能做些什麼。

什麼是 Perf ,它能做些什麼?

Perf 是 react 官方提供的效能分析工具,可以對我們的應用進行整體效能分析並提供效能資料。
直接來看下具體有哪些 API 吧:

  • Perf.start():開始測量。
  • Perf.stop():停止測量。
  • Perf.getLastMeasurements():在停止測量之後呼叫,用來獲取 measurements。

接下來就可以列印出效能資料了:

  • Perf.printInclusive(measurements):列印出所花費的整體時間。
  • Perf.printExclusive(measurements):列印出處理 props、getInitialState、呼叫 componentWillMount 和 componentDidMount 等的時間,這裡面不包含 mount 元件的時間。
  • Perf.printWasted(measurements):列印出測量時段內所浪費的時間。這部分資訊是分析資料中最有用的一部分了。我們可以通過這個資料找出時間被浪費在了哪兒。浪費一般出現在元件沒有渲染任何東西的時候,如上文中提到的,元件在 render 出新的虛擬 DOM 和舊的虛擬 DOM 對比之後,發現不需要更新元件。最理想的情況這個的返回值是一個空陣列。
  • Perf.printOperations(measurements):列印出分析時段內發生的底層 DOM 操作。

目前不少 React 效能優化的文件裡都有提到可以通過 shouldComponenentUpdate 和 Perf 來進行優化,但是卻沒有進行詳細的說明。一開始的時候我是困惑的:

  • Perf 是怎麼跑起來的?
  • 在什麼時候執行比較好?
  • 效能報表中的各個指標是什麼意思呢?
  • 怎麼結合這些資料來進行優化?

翻了不少文件並實踐之後,以我們到家業務的店鋪列表元件的優化為例,總結出來了以下的使用步驟,僅供大家參考。

Perf 怎麼用?

使用步驟:

步驟一:獲取

  • 先在頁面把原來的 react.js 替換成帶元件的版本 react-with-addons.js

    • 這裡要補充說明下關於使用的 react-with-addons 的版本

      • 推薦使用最新版本 15.3.2。
      • 如果使用 15.1.0 版本,react-with-addons 有可能會出現 Warning: There is an internal error in the React performance measurement code. We did not expect componentWillMount timer to stop while no timer is still in progress for another instance. Please report this as a bug in React。另外會出現有時候執行 React.addons.Perf.printOperations(measurements); 列印不出資訊來等一些奇怪的問題。
    • Perf 是在 0.11.0 版本 中新增的, 然後在 [15.1.0 版本]中進行了重構,並在後續版本中修復了不少 bug,目前還在逐漸完善的過程中。另外要注意的是:在非生產環境是不能使用 Perf 的。

步驟二:呼叫

方式一:直接在瀏覽器裡呼叫

  1. 在瀏覽器的控制檯裡輸入:
  React.addons.Perf.start(); 
  1. 執行某個操作,如滾動螢幕來載入列表
  2. 然後在控制檯裡輸入如下程式碼(以 printWasted 為例):
React.addons.Perf.stop();
var measurements = React.addons.Perf.getLastMeasurements(); 
React.addons.Perf.printWasted(measurements);

這樣就能夠看到列印出來這一過程所浪費的時間了。
_2016_09_22_8_34_02

方式二:新增到元件程式碼中

在元件的 componentDidUpdate 方法中呼叫,這樣可以在元件每次發生更新時列印出各個效能資料。

componentWillMount() {
  React.addons.Perf.start();
  // Your code
}
componentDidUpdate() {
  // Your code.
  let Perf = React.addons.Perf;
  Perf.stop();

  let measurements = React.addons.Perf.getLastMeasurements();
  if (measurements.length > 0) {
    Perf.printInclusive(measurements);
    Perf.printExclusive(measurements);
    Perf.printWasted(measurements);
    Perf.printOperations(measurements);

    Perf.start(); // clears measurements and try it again
  }
}

這樣就可以在頁面連續滾動時列印出多個資料。

接下來讓我們看下在這些資料中可以發現什麼。

資料指標分析

店鋪列表在每次下拉重新整理時,先變更列表載入狀態,再渲染出列表內容。以從第11頁下拉翻到第12頁為例,我們先來看下優化前後的效果對比圖,如下:
優化前:

  • Perf.printInclusive(measurements)
    _2016_09_22_8_49_29

_2016_09_22_8_49_53

  • Perf.printExclusive(measurements);
    _2016_09_22_8_50_12

_2016_09_22_8_50_28

  • Perf.printWasted(measurements)
    _2016_09_22_8_50_47

_2016_09_22_8_50_57

優化後:

  • Perf.printInclusive(measurements)
    _2016_09_22_8_54_32

_2016_09_22_8_54_44
_2016_09_22_8_54_59

  • Perf.printExclusive(measurements);
    _2016_09_22_8_55_19

_2016_09_22_8_55_28
_2016_09_22_8_55_42

  • Perf.printWasted(measurements)
    _2016_09_22_8_55_56

_2016_09_22_8_56_06
_2016_09_22_8_57_49

從上述圖表中可以看到,優化之後整體的渲染時間較時間有較大減少,且浪費時間的時間也大幅減少,在執行過程中,有個生命週期中的浪費時間已經減為0了。

下面就來看看這個優化是怎麼做的吧。

優化方案

拆分元件,結合 shouldComponentUpdate,以減少重繪次數。

  • 對於靜態元件,shouldComponentUpdate 返回 false;
  • 對於元件存在變化的情況

    • 如果變化的 props 或 state 不多,且層次不深,則可以在 shouldComponentUpdate(nextProps, nextState) 裡比較新老 props 和 state,在目標 props 或 state 發生變化時 return ture,其餘情況都 return false。
    • 如果變化的 props 和 state 多,或者層次深,則最好把元件拆分成變化的和不變化的部分。

注意:這裡必須要先確保元件是靜態的,即在 componentDidMount 後不會有任何變化,否則不能直接 return false。
在店鋪列表元件優化的過程中,一開始沒有留意到 ShopCard 元件中的優惠區域高度是會根據優惠條數的不同而有所不同的,並且具有收起和展開的功能,直接 return false 後導致這個區塊撐開的高度有問題了,並且收起/展開的功能也失效了。

  • 改出問題的樣子:

_2016_09_10_4_37_43

  • 正常情況初始時的樣子:

_2016_09_10_4_38_10

  • 正常情況展開後的樣子:

_2016_09_10_4_38_18

就拿 ShopCard 元件的程式碼作為例子看下 shouldComponentUpdate 是怎麼樣的吧:

  shouldComponentUpdate(nextProps, nextState) {
    let { shouldShowMoreActivities, height } = this.state;

    return shouldShowMoreActivities && height !== nextState.height;
  }

因為這個元件只會受是否有優惠活動和優惠撐開後的高度所影響,所以只要關注 shouldShowMoreActivities 和 height 這兩個 state 即可。

修改後整體效果如下:

  • Perf.printInclusive(measurements)
    _2016_09_22_11_17_07

_2016_09_22_11_17_20

  • Perf.printExclusive(measurements);
    _2016_09_22_11_17_34

_2016_09_22_11_17_46

  • Perf.printWasted(measurements)
    _2016_09_22_11_18_01

_2016_09_22_11_18_17

從優化後的效果圖中可以看到 ShopCard 元件只渲染了最後一頁增加的7項,另外,render time、render count 都從原來的上百減至幾個了,且浪費的時間也從原來的幾十毫秒減為個位數了。效果還是比較明顯的。
但是如果每個元件都要手動覆蓋 shouldComponentUpdate 方法也是比較費時的事情,並且這個方法的重寫也需要謹慎,可能會帶來意想不到的問題。
接下來讓我們看下 React 有沒有為這個事情做點什麼吧。

PureRenderMixin

如果你的元件在相同輸入的時候都能夠有相同的產出,那麼就可以使用 React 提供的 PureRenderMixin 外掛,它會自行為元件繫結 shouldComponentUpdate 方法,對現有的子元件的 state 和 props 進行判斷。但是它只支援基本型別的淺度比較,如果元件的 props 和 state 資料結構層次複雜則不適用。使用方法如下:

class Shop extends React.Component {
  constructor(props) {
    super(props);
    this.shouldComponentUpdate = React.addons.PureRenderMixin.shouldComponentUpdate.bind(this);
  }

  render() {
    // Your code
  }
}

說明:如果頁面上引入的是 react.js,可以自行安裝 react-addons-pure-render-mixin 依賴後以如下方式引入:

import PureRenderMixin from `react-addons-pure-render-mixin`;

效果如下圖(以 Perf.printWasted(measurements) 為例):
_2016_09_22_11_29_10
_2016_09_22_11_29_19

相對於最初版本的已經少了很多,不過比自己實現 shouldComponentUpdate 還是多浪費了 ShopCard 的 15 次 render。

React.PureComponent

在 react 的最新版本里面,還提供了 React.PureComponent 的基礎類,直接把原來的 React.Component 替換成 React.PureComponent 即可。
效果如下圖(以 Perf.printWasted(measurements)
為例):
_2016_09_22_11_42_18
_2016_09_22_11_42_29

效果和使用 PureRenderMixin 差不多。只是需要注意的是 PureComponent 是在 15.3.0 版本中才開始支援的。

另外,Facebook 還提供了一個專門處理不可變資料的庫 immutable.js ,大家感興趣的可自行了解。

清理元件之間不關聯的 props 對映

當父元件包含多個子元件,子元件之間存在互動的情況下,有些場景裡父元件只是受子元件的某一個屬性影響,或者一個子元件只受另外子元件的某些屬性影響,那麼在 mapStateToProps 的時候就要在各自的 Container 裡面把受影響元件的那幾個相關 state 對映到 props 裡。
但是元件一多,屬性一多,這就是件很費神的事情,尤其是寫的過程中發現要增加 state 了,就要在關聯元件的 mapStateToProps 中挨個加一遍,有時候發現某個屬性用不到了又要挨個刪一遍。不知道大家有沒有這種體驗,還是我的使用姿勢不對?反正每當這種時候我就特別想把要用到的狀態所屬的元件定義的整個 state 物件塞到自己的 props 裡,這樣不管後面加多少 state,也不用再加一遍,而是直接拿這個物件的屬性就好了。
但是,這樣會有一個副作用,某元件只要一個屬性更新了,對映了該元件所屬 state 到自己的 props 裡的元件就會觸發重新渲染了。而如上所說,shouldComponentUpdate 和 PureComponent 適用場景有限。因此,在程式碼層面能做的優化還是直接做掉吧,而且梳理一遍 props 和 state,可以對元件之間的互動邏輯更瞭解。
在簡化了 props 後,自己編寫 shouldComponentUpdate 也會簡單很多。

效果如下圖(以 Perf.printWasted(measurements) 為例):
_2016_09_22_11_48_21

從結果中可以看到少了很多浪費時間的專案。

React 頁面的效能優化方案還有很多,如合併 setState,合併 dispatch,漸進式渲染等,key,這裡就先不一一展開了,後續再講。

小結

本文主要講述瞭如何使用 Perf 效能分析工具結合 React 提供的 shouldComponentUpdate 方法、PureRenderMixin 外掛 和 PureComponent 元件來提高 React 元件的渲染效能。
還有其他很多工具如 Chrome 的 Timeline 和 Profiles 也能夠幫助我們發現程式碼中的問題。工具在很大程度上能夠給我們帶來效率上提升。
但在使用工具的同時,我們也要提高自己程式碼的質量,合理新增註釋,及時清理垃圾程式碼,優化程式碼,這樣不管是程式碼執行效率,還是後續的維護都能更高效。


相關文章