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

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

react性能瓶頸

要了解react的性能瓶頸,就須要知道react的渲染流程。它的渲染能夠分爲兩個階段:前端

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

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

咱們說的性能瓶頸指的是組件更新階段的狀況。linux

react組件更新流程

經過上面分析能夠知道組件更新具體過程以下:git

  • 執行該組件及其全部子組件的render方法獲取更新後的虛擬DOM,即re-render,即便子組件無需更新。github

  • 而後對新舊兩份虛擬DOM進行diff來進行組件的更新chrome

在這個過程當中,能夠經過組件的shouldComponentUpdate方法返回值來決定是否須要re-render。segmentfault

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性能優化的一個方向。

參考文獻

相關文章
相關標籤/搜索