在react專案中使用shouldComponentUpdate方法進行元件效能優化

龐順龍發表於2019-05-10

在react專案中使用shouldComponentUpdate方法進行元件效能優化,前半部分主要是思路和官方方法解釋,最後兩張大圖為測試對比結果。

react的渲染流程主要是

初始化元件

該階段會執行元件及其所有子元件的render方法,從而生成第一版的虛擬dom。

元件更新渲染

元件的props或者state任意發生改變就會觸發元件的更新渲染。預設情況下其也會執行該元件及其所有子元件的render方法獲取新的虛擬dom。

接下來我們聊聊元件更新渲染的效能問題。

react元件更新流程

通過上面分析可以知道元件更新具體過程如下:

執行該元件及其所有子元件的render方法獲取更新後的虛擬DOM,即re-render,即使子元件無需更新。
然後對新舊兩份虛擬DOM進行diff來進行元件的更新
在這個過程中,可以通過元件的shouldComponentUpdate方法返回值來決定是否需要re-render。

react的整個更新渲染流程可以借用一張圖來加以說明:


預設地,元件的shouldComponentUpdate返回true,即React預設會呼叫所有元件的render方法來生成新的虛擬DOM, 然後跟舊的虛擬DOM比較來決定元件最終是否需要更新。

效能瓶頸分析

借圖說話,例如下圖是一個元件結構tree,當我們要更新某個子元件的時候,如下圖的綠色元件(從根元件傳遞下來應用在綠色元件上的資料發生改變):

理想情況下,我們只希望關鍵路徑上的元件進行更新,如下圖:


但是,實際效果卻是每個元件都完成re-render和virtual-DOM diff過程,雖然元件沒有變更,這明顯是一種浪費。如下圖黃色部分表示浪費的re-render和virtual-DOM diff。


根據上面的分析,react的效能瓶頸主要表現在:
對於props和state沒有變化的元件,react也要重新生成虛擬DOM及虛擬DOM的diff。
這個時候,就是shouldComponentUpdate上場的時候了。

shouldComponentUpdate(nextProps, nextState){
   return !isEqual(nextProps, this.props) || !isEqual(nextState, this.state)
}
其中,isEqual方法為判斷兩個物件是否相等(指的是其物件內容相等,而不是全等)。

通過顯示覆蓋shouldComponentUpdate方法來判斷元件是否需要更新從而避免無用的更新,但是若為每個元件新增該方法會顯得繁瑣,好在react提供了官方的解決方案,具體做法:

方案對元件的shouldComponentUpdate進行了封裝處理,實現對元件的當前屬性和狀態與上一次的進行淺對比,從而決定元件是否需要更新。

react在發展的不同階段提供兩套官方方案:

PureRenderMin


一種是基於ES5的React.createClass建立的元件,配合該形式下的mixins方式來組合PureRenderMixin提供的shouldComponentUpdate方法。當然用ES6建立的元件也能使用該方案。


import PureRenderMixin from 'react-addons-pure-render-mixin';
class Example extends React.Component {
  constructor(props) {
    super(props);
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
}
PureComponent


該方案是在React 15.3.0版本釋出的針對ES6而增加的一個元件基類:React.PureComponent。這明顯對ES6方式建立的元件更加友好。


import React, { PureComponent } from 'react'
class Example extends PureComponent {
  render() {
    // ...
  }
}
需要指出的是,不管是PureRenderMin還是PureComponent,他們內部的shouldComponentUpdate方法都是淺比較(shallowCompare)props和state物件的,即只比較物件的第一層的屬性及其值是不是相同。例如下面state物件變更為如下值:



state = {
  value: { foo: 'bar' }
}
因為state的value被賦予另一個物件,使nextState.value與this.props.value始終不等,導致淺比較通過不了。在實際專案中,這種巢狀的物件結果是很常見的,如果使用PureRenderMin或者PureComponent方式時起不到應有的效果。
雖然可以通過深比較方式來判斷,但是深比較類似於深拷貝,遞迴操作,效能開銷比較大。
為此,可以對元件儘可能的拆分,使元件的props和state物件資料達到扁平化,結合著使用PureRenderMin或者PureComponent來判斷元件是否更新,可以更好地提升react的效能,不需要開發人員過多關心。

案例分析

TimeLineView 列表頁元件面
TimeLineEventView 列表item元件頁面
說明:列表頁面 資料list.map(TimeLineEventView)

現有一個列表頁面,滾動載入下一頁,每個專案中均有一個點贊功能,按照邏輯我們一般是在列表jsx頁面通過資料list.map(頁面item方法)來實現,通過頁面state來處理點贊按鈕的樣式切換:

我們不對列表item進行shouldComponentUpdate處理,下面通過Perf工具來檢測當我們點選某一個item贊功能的時候,通過Perf輸出頁面效能檢測結果:


如上圖的Print Wasted 模組,我們可以看到當前列表頁面,我們點選某一個item的贊功能,頁面會產生將近20ms的效能損耗,主要就是item元件 TimeLineEventView 被重新渲染了17次,而這17次都是沒有任何變化的item產生的。
然後我們改造TimeLineEventView頁面,增加shouldComponentUpdate處理:


shouldComponentUpdate(nextProps, nextState) {
    if (this.props.IsPraised == nextProps.IsPraised) {
      return false;
    }
    return true;
  }
上面的程式碼就是充寫了react預設返回true的shouldComponentUpdate方法,通過邏輯可知,只有被點選的item元件才會返回true,其他均為false,這樣就會實現非相關元件不會重新render渲染,改造完畢後,我們再次通過Perf工具進行檢測測試,結果如下:


繼續看上圖的Print Wasted 模組,可以看到,已經沒有 TimeLineEventView 相關的元件渲染損耗記錄了,說明我們優化成功,目前的渲染損耗在1ms左右,雖然從 20ms 到 1ms並不會對使用者產生很大的感知,但是對於滾動載入列表來說,如果資料結構複雜,頁面資料量達到幾百條,那麼損耗的效能可能就是7.8百ms,對於頁面來說就會很明顯的卡頓等,所以,對元件進行足夠、合理的扁平化拆分就顯得尤為必要了。

參考:
http://www.cnblogs.com/wonyun/p/6804952.html
http://benchling.engineering/deep-dive-react-perf-debugging/
https://segmentfault.com/a/1190000006741060

React Perf chrome外掛地址:
https://chrome.google.com/webstore/detail/react-perf/hacmcodfllhbnekmghgdlplbdnahmhmm


內容均為作者獨立觀點,不代表八零IT人立場,如涉及侵權,請及時告知。

相關文章