React高階效能優化

hepeguo發表於2016-08-16

當大家考慮在專案中使用 React 的時候,第一個問題往往是他們的應用的速度和響應是否能和非 React 版一樣,每當狀態改變的時候就重新渲染元件的整個子樹,讓大家懷疑這會不會對效能造成負面影響。React 用了一些黑科技來減少 UI 更新需要的花費較大的 DOM 操作。

使用 production 版本

如果你在你的 React app 中進行效能測試或在尋找效能問題,一定要確定你在使用 minified production build。開發者版本包括額外的警告資訊,這對你在開發你的 app 的時候很有用,但是因為要進行額外的處理,所以它也會比較慢。

避免更新 DOM

React 使用虛擬 DOM,它是在瀏覽器中的 DOM 子樹的渲染描述,這個平行的描述讓 React 避免建立和操作 DOM 節點,這些遠比操作一個 JavaScript 物件慢。當一個元件的 props 或 state 改變,React 會構造一個新的虛擬 DOM 和舊的進行對比來決定真實 DOM 更新的必要性,只有在它們不相等的時候,React 才會使用盡量少的改動更新 DOM。

在此之上,React 提供了生命週期函式 shouldComponentUpdate,在重新渲染機制迴路(虛擬 DOM 對比和 DOM 更新)之前會被觸發,賦予開發者跳過這個過程的能力。這個函式預設返回 true,讓 React 執行更新。

shouldComponentUpdate: function(nextProps, nextState) {
  return true;
}

一定要記住,React 會非常頻繁的呼叫這個函式,所以要確保它的執行速度夠快。

假如你有個帶有多個對話的訊息應用,如果只有一個對話發生改變,如果我們在 ChatThread 元件執行 shouldComponentUpdate,React 可以跳過其他對話的重新渲染步驟。

shouldComponentUpdate: function(nextProps, nextState) {
  // TODO: return whether or not current chat thread is
  // different to former one.
}

因此,總的說,React 通過讓使用者使用 shouldComponentUpdate 減短重新渲染迴路,避免進行昂貴的更新 DOM 子樹的操作,而且這些必要的更新,需要對比虛擬 DOM。

shouldComponentUpdate 實戰

這裡有個元件的子樹,每一個都指明瞭 shouldComponentUpdate 返回值和虛擬 DOM 是否相等,最後,圓圈的顏色表示元件是否需要更新。

在上面的示例中,因為 C2 的 shouldComponentUpdate 返回 false,React 就不需要生成新的虛擬 DOM,也就不需要更新 DOM,注意 React 甚至不需要呼叫 C4 和 C5 的 shouldComponentUpdate

C1 和 C3 的 shouldComponentUpdate 返回 true,所以 React 需要向下到葉子節點檢查它們,C6 返回 true,因為虛擬 DOM 不相等,需要更新 DOM。最後感興趣的是 C8,對於這個節點,React 需要計算虛擬 DOM,但是因為它和舊的相等,所以不需要更新 DOM。

注意 React 只需要對 C6 進行 DOM 轉換,這是必須的。對於 C8,通過虛擬 DOM 的對比確定它是不需要的,C2 的子樹和 C7,它們甚至不需要計算虛擬 DOM,因為 shouldComponentUpdate

那麼,我們怎麼實現 shouldComponentUpdate 呢?比如說你有一個元件僅僅渲染一個字串:

React.createClass({
  propTypes: {
    value: React.PropTypes.string.isRequired
  },

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

我們可以簡單的實現 shouldComponentUpdate 如下:

shouldComponentUpdate: function(nextProps, nextState) {
  return this.props.value !== nextProps.value;
}

非常好!處理這樣簡單結構的 props/state 很簡單,我門甚至可以歸納出一個基於淺對比的實現,然後把它 Mixin 到元件中。實際上 React 已經提供了這樣的實現: PureRenderMixin

但是如果你的元件的 props 或者 state 是可變的資料結構呢?比如說,元件接收的 prop 不是一個像 'bar' 這樣的字串,而是一個包涵字串的 JavaScript 物件,比如 { foo: 'bar' }:

React.createClass({
  propTypes: {
    value: React.PropTypes.object.isRequired
  },

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

前面的 shouldComponentUpdate 實現就不會一直和我們期望的一樣工作:

// assume this.props.value is { foo: 'bar' }
// assume nextProps.value is { foo: 'bar' },
// but this reference is different to this.props.value
this.props.value !== nextProps.value; // true

這個問題是當 prop 沒有改變的時候 shouldComponentUpdate 也會返回 true。為了解決這個問題,我們有了這個替代實現:

shouldComponentUpdate: function(nextProps, nextState) {
  return this.props.value.foo !== nextProps.value.foo;
}

基本上,我們結束了使用深度對比來確保改變的正確跟蹤,這個方法在效能上的花費是很大的,因為我們需要為每個 model 寫不同的深度對比程式碼。就算這樣,如果我們沒有處理好物件引用,它甚至不能工作,比如說這個父元件:

React.createClass({
  getInitialState: function() {
    return { value: { foo: 'bar' } };
  },

  onClick: function() {
    var value = this.state.value;
    value.foo += 'bar'; // ANTI-PATTERN!
    this.setState({ value: value });
  },

  render: function() {
    return (
      <div>
        <InnerComponent value={this.state.value} />
        <a onClick={this.onClick}>Click me</a>
      </div>
    );
  }
});

內部元件第一次渲染的時候,它會獲取 { foo: 'bar' } 作為 value 的值。如果使用者點選了 a 標籤,父元件的 state 會更新成 { value: { foo: 'barbar' } },觸發內部元件的重新渲染過程,內部元件會收到 { foo: 'barbar' } 作為 value 的新的值。

這裡的問題是因為父元件和內部元件共享同一個物件的引用,當物件在 onClick 函式的第二行發生改變的時候,內部元件的屬性也發生了改變,所以當重新渲染過程開始,shouldComponentUpdate 被呼叫的時候,this.props.value.foo 和 nextProps.value.foo是相等的,因為實際上 this.props.value 和 nextProps.value 是同一個物件的引用。

因此,我們會丟失 prop 的改變,縮短重新渲染過程,UI 也不會從 'bar' 更新到 'barbar'

Immutable-js 來救贖

Immutable-js 是 Lee Byron 寫的 JavaScript 集合型別的庫,最近被 Facebook 開源,它通過結構共享提供不可變持久化集合型別。一起看下這些特性的含義:

  • Immutable: 一旦建立,集合就不能再改變。
  • Persistent: 新的集合型別可以通過之前的集合建立,比如 set 產生改變的集合。建立新的集合之後源集合仍然有效。
  • Structural Sharing: 新的集合會使用盡量多的源集合的結構,減少複製來節省空間和效能友好。如果新的集合和源集合相等,一般會返回源結構。

不可變讓跟蹤改變非常簡單;每次改變都是產生新的物件,所以我們僅需要物件的引用是否改變,比如這段簡單的 JavaScript 程式碼:

var x = { foo: "bar" };
var y = x;
y.foo = "baz";
x === y; // true

儘管 y 被改變,因為它和 x 引用的是同一個物件,這個對比返回 true。然而,這個程式碼可以使用 immutable-js 改寫如下:

var SomeRecord = Immutable.Record({ foo: null });
var x = new SomeRecord({ foo: 'bar'  });
var y = x.set('foo', 'baz');
x === y; // false

這個例子中,因為改變 x 的時候返回了新的引用,我們就可以安全的認為 x 已經改變。

髒檢測可以作為另外的可行的方式追蹤改變,給 setters 一個標示。這個方法的問題是,它強制你使用 setters,而且要寫很多額外的程式碼,影響你的類。或者你可以在改變之前深拷貝物件,然後進行深對比來確定是不是發生了改變。這個方法的問題是,深拷貝和深對比都是很花效能的操作。

因此,不可變資料結構給你提供了一個高效、簡潔的方式來跟蹤物件的改變,而跟蹤改變是實現 shouldComponentUpdate 的關鍵。所以,如果我們使用 immutable-js 提供的抽象建立 props 和 state 模型,我們就可以使用 PureRenderMixin,而且能夠獲得很好的效能增強。

Immutable-js 和 Flux

如果你在使用 Flux,你應該開始使用 immutable-js 寫你的 stores,看一下 full API。

讓我們看一個可行的方式,使用不可變資料結構來給訊息示例建立資料結構。首先我們要給每個要建模的實體定義一個 Record。Records 僅僅是一個不可變容器,裡面儲存一系列具體資料:

var User = Immutable.Record({
  id: undefined,
  name: undefined,
  email: undefined
});

var Message = Immutable.Record({
  timestamp: new Date(),
  sender: undefined,
  text: ''
});

Record 方法接收一個物件,來定義欄位和對應的預設資料。

訊息的 store 可以使用兩個 list 來跟蹤 users 和 messages:

this.users = Immutable.List();
this.messages = Immutable.List();

實現函式處理每個 payload 型別應該是比較簡單的,比如,當 store 看到一個代表新訊息的 payload 時,我們就建立一個新的 record,並放入訊息列表:

this.messages = this.messages.push(new Message({
  timestamp: payload.timestamp,
  sender: payload.sender,
  text: payload.text
});

注意:因為資料結構不可變,我們需要把 push 方法的結果賦給 this.messages

在 React 裡,如果我們也使用 immutable-js 資料結構來儲存元件的 state,我門可以把 PureRenderMixin 混入到我門所有的元件來縮短重新渲染迴路。

相關文章