從性能角度看react組件拆分的重要性


React是一個UI層面的庫,它采用虛擬DOM技術減少Javascript與真正DOM的交互,提升了前端性能;采用單向數據流機制,父組件通過props將數據傳遞給子組件,這樣讓數據流向一目了然。一旦組件的props或則state發生改變,組件及其子組件都將重新re-render和vdom-diff,從而完成數據的流向交互。但是這種機制在某些情況下比如說數據量較大的情況下可能會存在一些性能問題。下面就來分析react的性能瓶頸,並用結合着react-addons-perf工具來說明react組件拆分的重要性。

react性能瓶頸

要了解react的性能瓶頸,就需要知道react的渲染流程。它的渲染可以分為兩個階段:

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

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

我們說的性能瓶頸指的是組件更新階段的情況。

react組件更新流程

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

  • 執行該組件及其所有子組件的render方法獲取更新后的虛擬DOM,即re-render,即使子組件無需更新。

  • 然后對新舊兩份虛擬DOM進行diff來進行組件的更新

在這個過程中,可以通過組件的shouldComponentUpdate方法返回值來決定是否需要re-render。

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

默認地,組件的shouldComponentUpdate返回true,即React默認會調用所有組件的render方法來生成新的虛擬DOM, 然后跟舊的虛擬DOM比較來決定組件最終是否需要更新。

react性能瓶頸

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

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

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

根據上面的分析,react的性能瓶頸主要表現在:

對於propsstate沒有變化的組件,react也要重新生成虛擬DOM及虛擬DOM的diff。

shouldComponentUpdate來進行性能優化

針對react的性能瓶頸,我們可以通過react提供的shouldComponentUpdate方法來做點優化的事,可以有選擇的進行組件更新,從而提升react的性能,具體如下:

shouldComponentUpdate需要判斷當前屬性和狀態是否和上一次的相同,如果相同則不需要執行后續生成虛擬DOM及其diff的過程,否則需要更新。

具體可以這么顯示實現:

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)propsstate對象的,即只比較對象的第一層的屬性及其值是不是相同。例如下面state對象變更為如下值:

state = {
  value: { foo: 'bar' }
}

因為state的value被賦予另一個對象,使nextState.valuethis.props.value始終不等,導致淺比較通過不了。在實際項目中,這種嵌套的對象結果是很常見的,如果使用PureRenderMin或者PureComponent方式時起不到應有的效果。

雖然可以通過深比較方式來判斷,但是深比較類似於深拷貝,遞歸操作,性能開銷比較大。

為此,可以對組件盡可能的拆分,使組件的propsstate對象數據達到扁平化,結合着使用PureRenderMin或者PureComponent來判斷組件是否更新,可以更好地提升react的性能,不需要開發人員過多關心。

組件拆分

組件拆分,在react中就是將組件盡可能的細分,便於復用和優化。拆分的具體原則:

  • 盡量使拆分后的組件更容易判斷是否更新

這不太好理解,舉個例子吧:假設我們定義一個父組件,其包含了5000個子組件。有一個輸入框輸入操作,每次輸入一個數字,對應的那個子組件背景色變紅。

<div>
    <input value={this.state.inputText} onChange={this.inputChanged}/>
    <ul
    {
     this.state.items.map(el=>
         <li key={el.id} style={{background: index===this.state.inputText? 'red' : ''}}>{el.name}</li>
    }
    </ul>
</div>

本例中,輸入框組件和列表子組件有着明顯的不同,一個是動態的,輸入值比較頻繁;一個是相對靜態的,不管input怎么輸入它就是5000項。輸入框每輸入一個數字都會導致所有組件re-render,這樣就會造成列表子組件不必要的更新。

可以看出,上面列表組件的更新不容易被取消,因為輸入組件和列表子組件的狀態都置於父組件state中,二者共享;react不可能用shouldComponentUpdate的返回值來使組件一部分組件更新,另一部分不更新。 只有把他們拆分為不同的組件,每個組件只關心對應的props。拆分的列表組件只關心自己那部分屬性,其他組件導致父組件的更新在列表組件中可以通過判斷自己關心的屬性值情況來決定是否更新,這樣才能更好地進行組件優化。

  • 盡量使拆分組件的props和state數據扁平化

這主要是從組件優化的角度考慮的,如果組件不需過多關注性能,可以忽略。

拆分組件之所以扁平化,是因為React提供的優化方案PureRenderMin或者PureComponent是淺比較組件的propsstate來決定是否更新組件。

上面的列表組件中,this.state.items存放的是對象數組,為了更好的判斷每項列表是否需要更新,可以將每個li列表項單獨拆分為一個列表項組件,每個列表項相關的props就是items數組中的每個對象,這種扁平化數據很容易判斷是否數據發生變化。

組件拆分的一個例子

為了這篇文章專門寫了一個有關添加展示Todo列表的事例庫。克隆代碼到本地可以在本地運行效果。

該事例庫是一個有着5000項的Todo列表,可以刪除和新增Todo項。該事例展示了組件拆分前和拆分后的體驗對比情況,可以發現有性能明顯的提升。

下面我們結合react的性能檢測工具react-addons-perf來說明組件拆分的情況。

拆分前的組件TodosBeforeDivision的render部分內容如下:

<input value={this.state.value} onChange={this.inputChange.bind(this)}/>
<button onClick={this.addTodo.bind(this)}>add todo</button>
{
    this.state.items.map(el=>{
        return (
          <TodoItem key={el.id} item={el} 
            tags={['important', 'starred']}
            deleteItem={this.deleteItem.bind(this, el.id)}/>)
      })
}

組件拆分前,輸入框輸入字符、增加todo或者刪除todo項可以看出有明顯的卡頓現象,如下圖所示:

為了弄清楚是什么原因導致卡頓現象,我們使用chrome的devTool來定位,具體的做法是使用最新版的chrome瀏覽器的Performance選項來完成。先點擊該選項中的record按鈕開始記錄,這時我們在組件輸入框輸入一個字符,然后點擊stop來停止記錄,我們會看到組件從輸入開始到結束這段時間內的一個性能profile。

從圖可以看出我們在輸入單個字符時,輸入框的input事件邏輯幾乎占據整個響應時間,具體的處理邏輯主要是react層面的batchedUpdates方法批量更新列表組件,而不是用戶自定義的邏輯。

那么,批量更新為啥占據這么多時間呢,為了搞清楚原因,我們借助基於react-addons-perf的chrome插件chrome-react-perf,它以chrome插件的形式輸出分析的結果。

使用該插件需要注意一點的是:

chrome-react-perf插件的使用需要在項目中引入react-addons-perf模塊,並必須將其對象掛載到window全局對象的Perf屬性上,否則不能使用。

在devTool工具中選擇Perf選項試圖,點擊start按鈕后其變成stop按鈕,在組件輸入框中輸入一個字符,然后點擊Perf試圖中的stop按鈕,就會得出對應的性能試圖。

上圖提供的4個視圖中,Print Wasted對分析性能最有幫組,它表示組件沒有變化但是參與了更新過程,即浪費了re-render和vdom-diff這一過程,是毫無意義的過程。從圖可以看出:TodosBeforeDivisionTodoItem組件分別浪費了167.88ms、144.47ms,這完全可以通過拆分組件避免的開銷,這是react性能優化重點。

為此我們需要對TodosBeforeDivision組件進行拆分,拆分為一個帶有input和button的動態組件AddTodoForm和一個相對靜態的組件TodoList。二者分別繼承React.PureComponent可以避免不必要的組件更新。

export default class AddTodoForm extends React.PureComponent{
...
render(){
    return (
      <form>
        <input value={this.state.value} onChange={this.inputChange}/>
        <button onClick={this.addTodo}>add todo</button>
      </form>
    )
  }
...
}

其中TodoList組件還需要為每項Todo任務拆分為一個組件TodoItem,這樣每個TodoItem組件的props對象為扁平化的數據,可以充分利用React.PureComponent來進行對象淺比較從而更好地決定組件是否要更新,這樣避免了新增或者刪除一個TodoItem項時,其他TodoItem組件不必更新。

export default class TodoList extends React.PureComponent{
  ...  
  render(){
    return (
      <div>
        {this.props.initailItems.map(el=>{
          return <TodoItem key={el.id} item={el} tags={this.props.tags} deleteItem={this.props.deleteItem}/>
        })}
      </div>
    )
  }
 ...
}

export default class TodoItem extends React.PureComponent{
  ...
  render(){
    return (
      <div>
        <button style={{width: 30}} onClick={this.deleteItem}>x</button>
        <span>{this.props.item.text}</span>
        {this.props.tags.map((tag) => {
          return <span key={tag} className="tag"> {tag}</span>;
        })}
      </div>
    )
  }
...
}

這樣拆分后的組件,在用上面的性能檢測工具查看對應的效果:

圖片描述

圖片描述

從上面的截圖可以看出,拆分后的組件性能有了上百倍的提升,雖然其中還包含一些其他優化,例如不將function在組件屬性位置綁定this以及常量對象props緩存起來等避免每次re-render時重新生成新的function和新的對象props。

總的來說,對react組件進行拆分對react性能的提升是非常重要的,這也是react性能優化的一個方向。

參考文獻


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM