React性能優化總結

from:http://www.tuicool.com/articles/VNFZBbj

初學者對React可能滿懷期待,覺得React可能完爆其它一切框架,甚至不切實際地認爲React可能連原生的渲染都能完爆——對框架的狂熱確實會出現這樣的不切實際的期待。讓我們來看看React的官方是怎麼說的。React官方文檔在Advanced Performanec這一節,這樣寫道:

One of the first questions people ask when considering React for a project is whether their application will be as fast and responsive as an equivalent non-React version

顯然React自己也其實只是想盡量達到跟非React版本相若的性能,

你所不知道的render

react的組件渲染分爲初始化渲染和更新渲染。

在初始化渲染的時候會調用根組件下的所有組件的render方法進行渲染,如下圖(綠色表示已渲染,這一層是沒有問題的):

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

我們的理想狀態是隻調用關鍵路徑上組件的render,如下圖:

但是react的默認做法是調用所有組件的render,再對生成的虛擬DOM進行對比,如不變則不進行更新。這樣的render和虛擬DOM的 對比 明顯是在浪費,如下圖(黃色表示浪費的render和虛擬DOM對比)

Tips:

  • 拆分組件是有利於複用和組件優化的。

  • 生成虛擬DOM並進行比對發生在render()後,而不是render()前。

更新階段的生命週期

  • componentWillReceiveProps(object nextProps) :當掛載的組件接收到新的props時被調用。此方法應該被用於比較this.props 和 nextProps以用於使用this.setState()執行狀態轉換。(組件內部數據有變化,使用state,但是在更新階段又要在props改變的時候改變state,則在這個生命週期裏面)

  • shouldComponentUpdate(object nextProps, object nextState) : -boolean 當組件決定任何改變是否要更新到DOM時被調用。作爲一個 優化 實現比較this.props 和 nextProps 、this.state 和 nextState ,如果React應該跳過更新,返回false。

  • componentWillUpdate(object nextProps, object nextState) :在更新發生前被立即調用。你不能在此調用 this.setState() 。

  • componentDidUpdate(object prevProps, object prevState) : 在更新發生後被立即調用。(可以在DOM更新完之後,做一些收尾的工作)

Tips:

  • React的優化是基於 shouldComponentUpdate 的,該生命週期默認返回true,所以一旦prop或state有任何變化,都會引起重新render。

shouldComponentUpdate

react在每個組件生命週期更新的時候都會調用一個shouldComponentUpdate(nextProps, nextState)函數。它的職責就是返回true或false,true表示需要更新,false表示不需要,默認返回爲true,即便你沒有顯示地定義 shouldComponentUpdate 函數。這就不難解釋上面發生的資源浪費了。

爲了進一步說明問題,我們再引用一張官網的圖來解釋,如下圖( SCU表示shouldComponentUpdate,綠色表示返回true(需要更新),紅色表示返回false(不需要更新);vDOMEq表示虛擬DOM比對,綠色表示一致(不需要更新),紅色表示發生改變(需要更新)):

根據渲染流程,首先會判斷shouldComponentUpdate(SCU)是否需要更新。如果需要更新,則調用組件的render生成新的虛擬DOM,然後再與舊的虛擬DOM對比(vDOMEq),如果對比一致就不更新,如果對比不同,則根據最小粒度改變去更新DOM;如果SCU不需要更新,則直接保持不變,同時其子元素也保持不變。

  • C1根節點,綠色SCU (true),表示需要更新,然後vDOMEq紅色,表示虛擬DOM不一致,需要更新。

  • C2節點,紅色SCU (false),表示不需要更新,所以C4,C5均不再進行檢查

  • C3節點同C1,需要更新

  • C6節點,綠色SCU (true),表示需要更新,然後vDOMEq紅色,表示虛擬DOM不一致,更新DOM。

  • C7節點同C2

  • C8節點,綠色SCU (true),表示需要更新,然後vDOMEq綠色,表示虛擬DOM一致,不更新DOM。

帶坑的寫法:

  • {...this.props} (不要濫用,請只傳遞component需要的props,傳得太多,或者層次傳得太深,都會加重shouldComponentUpdate裏面的數據比較負擔,因此,也請慎用spread attributes(<Component {...props} />))。

  • ::this.handleChange()。(請將方法的bind一律置於constructor)

  • this.handleChange.bind(this,id)

  • 複雜的頁面不要在一個組件裏面寫完。

  • 請儘量使用const element。

  • map裏面添加key,並且key不要使用index(可變的)。具體可參考 使用Perf工具研究React Key對渲染的影響

  • 儘量少用setTimeOut或不可控的refs、DOM操作。

  • 數據儘可能簡單明瞭,扁平化。

 

不論你用不用immutable,只要你想達到pure render,下面值得你注意!!!
一天我和往常一樣,開開心心得寫着react,用着@pureRender,

export default class  extends Component {
...
  render() {
    const {name,age} =this.state;
    return (
      <div>
        <Person name={name} age={age} onClick={this._handleClick.bind(this)}></Person>//bug 所在
      </div>
    )
  }
...
}

發現一個問題,對於Person這個子組件來說,在父組件re-render的時候,即使Person得前後兩個props都沒改變,它依舊會re-render。。即使用immutable.js也不好使。。。
原來啊,父組件每次render,_handleClick都會執行bind(this) 這樣_handleClick的引用每次都會改。。所以Person前後兩次props其實是不一樣的。。
那怎麼辦?把bind(this)去掉?不行 還必須得用
真正的答案是 讓父組件每次render 不執行bind(this),直接提前在定義class裏面的constructor執行好

export default class  extends Component {
  constructor(props){
    super(props)
    this._handleClick=this._handleClick.bind(this)//改成這樣
  }
  render() {
    const {name,age} =this.state;
    return (
      <div>
        <Person name={name} age={age} onClick={this._handleClick}></Person>
      </div>
    )
  }
...
}

 

 

性能檢測工具

React.addons.Perf

react官方提供一個插件 React.addons.Perf 可以幫助我們分析組件的性能,以確定是否需要優化。

打開console面板,先輸入 Perf.start() 執行一些組件操作,引起數據變動,組件更新,然後輸入 Perf.stop() 。(建議一次只執行一個操作,好進行分析)

再輸入 Perf.printInclusive 查看所有涉及到的組件render,如下圖(官方圖片):

或者輸入Perf.printWasted()查看下不需要的的浪費組件render,如下圖(官方圖片):

優化前:

優化後:

其他的檢測工具

react-perf-tool 爲React應用提供了一種可視化的性能檢測方案,該工程同樣是基於React.addons,但是使用圖表來顯示結果,更加方便。

React官方的解決方案

PureRenderMixin(es5)

var PureRenderMixin = require('react-addons-pure-render-mixin');
React.createClass({
  mixins: [PureRenderMixin],

  render: function() {
    return <div className={this.props.className}>foo</div>;
  }
});

Shallow Compare (es6)

var shallowCompare = require('react-addons-shallow-compare');
export class SampleComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    return shallowCompare(this, nextProps, nextState);
  }

  render() {
    return <div className={this.props.className}>foo</div>;
  }
}

es7裝飾器的寫法:

import pureRender from "pure-render-decorator"
...

@pureRender
class Person  extends Component {
  render() {
    console.log("我re-render了");
    const {name,age} = this.props;

      return (
        <div>
          <span>姓名:</span>
          <span>{name}</span>
          <span> age:</span>
          <span>{age}</span>
        </div>
      )
  }
}

pureRender很簡單,就是把傳進來的component的shouldComponentUpdate給重寫掉了,原來的shouldComponentUpdate,無論怎樣都是return ture,現在不了,我要用shallowCompare比一比,shallowCompare代碼及其簡單,如下:

function shallowCompare(instance, nextProps, nextState) {
  return !shallowEqual(instance.props, nextProps) || !shallowEqual(instance.state, nextState);
}

缺點

shallowEqual其實只比較props的第一層子屬性是不是相同,就像上述代碼,props 是如下

{
  detail: {
    name: "123",
    age: "123"
  }
}

他只會比較props.detail ===nextProps.detail,導致在傳入複雜的數據的情況下,優化失效。

immutable.js

我們也可以在 shouldComponentUpdate() 中使用使用 deepCopy 和 deepCompare 來避免無必要的 render(),但 deepCopy 和 deepCompare 一般都是非常耗性能的。

Immutable Data 就是一旦創建,就不能再被更改的數據。對 Immutable 對象的任何修改或添加刪除操作都會返回一個新的 Immutable 對象。

Immutable 實現的原理是 Persistent Data Structure (持久化數據結構),也就是使用舊數據創建新數據時,要保證舊數據同時可用且不變。同時爲了避免 deepCopy 把所有節點都複製一遍帶來的性能損耗,Immutable 使用了 Structural Sharing (結構共享),即如果對象樹中一個節點發生變化,只修改這個節點和受它影響的父節點,其它節點則進行共享。請看下面動畫:

Immutable 則提供了簡潔高效的判斷數據是否變化的方法,只需 === 和 is 比較就能知道是否需要執行 render(),而這個操作幾乎 0 成本,所以可以極大提高性能。修改後的 shouldComponentUpdate 是這樣的:

import { is } from 'immutable';

shouldComponentUpdate: (nextProps = {}, nextState = {}) => {
  const thisProps = this.props || {}, thisState = this.state || {};

  if (Object.keys(thisProps).length !== Object.keys(nextProps).length ||
      Object.keys(thisState).length !== Object.keys(nextState).length) {
    return true;
  }

  for (const key in nextProps) {
    if (!is(thisProps[key], nextProps[key])) {
      return true;
    }
  }

  for (const key in nextState) {
    if (thisState[key] !== nextState[key] || !is(thisState[key], nextState[key])) {
      return true;
    }
  }
  return false;
}

react-immutable-render-mixin

這是一個facebook/immutable-js的react pure render mixin 的庫,可以簡化很多寫法。

使用 react-immutable-render-mixin 可以實現裝飾器的寫法。

import React from 'react';
import { immutableRenderDecorator } from 'react-immutable-render-mixin';

@immutableRenderDecorator
class Test extends React.Component {
  render() {
    return <div></div>;
  }
}

Immutable 詳解及 React 中實踐

無狀態組件

爲了避免一定程度的浪費,react官方還在0.14版本中加入了 無狀態組件 ,如下:

// es6
const HelloMessage = (props) => <div>Hello {props.name}</div>;

高階組件

大部分使用mixin和class extends的地方,高階組件都是更好的方案——畢竟 組合優於繼承 。

使用ES6編寫React應用(4):使用高階組件替代Mixins

Mixin 已死,Composition 萬歲

React同構直出

同構基於服務端渲染,卻不止是服務端渲染。

React在減少重複渲染方面確實是有一套獨特的處理辦法,那就是virtual DOM,但顯示在首次渲染的時候React絕無可能超越原生的速度,或者一定能將其它的框架比下去。因此,我們在做優化的時候,可的期待的東西有:

  • 首屏時間可能會比較原生的慢一些,但可以嘗試用React Server Render (又稱Isomorphic)去提高效率

  • 用戶進行交互的時候,有可能會比原生的響應快一些,前提是你做了一些優化避免了浪費性能的重複渲染。