react性能優(yōu)化

React性能優(yōu)化

hepeguo 2016年08月12日發(fā)布

當(dāng)大家考慮在項(xiàng)目中使用 React 的時(shí)候,第一個(gè)問題往往是他們的應(yīng)用的速度和響應(yīng)是否能和非 React 版一樣,每當(dāng)狀態(tài)改變的時(shí)候就重新渲染組件的整個(gè)子樹,讓大家懷疑這會(huì)不會(huì)對(duì)性能造成負(fù)面影響。React 用了一些黑科技來(lái)減少 UI 更新需要的花費(fèi)較大的 DOM 操作。

使用 production 版本

如果你在你的 React app 中進(jìn)行性能測(cè)試或在尋找性能問題,一定要確定你在使用 minified production build。開發(fā)者版本包括額外的警告信息,這對(duì)你在開發(fā)你的 app 的時(shí)候很有用,但是因?yàn)橐M(jìn)行額外的處理,所以它也會(huì)比較慢。

避免更新 DOM

React 使用虛擬 DOM,它是在瀏覽器中的 DOM 子樹的渲染描述,這個(gè)平行的描述讓 React 避免創(chuàng)建和操作 DOM 節(jié)點(diǎn),這些遠(yuǎn)比操作一個(gè) JavaScript 對(duì)象慢。當(dāng)一個(gè)組件的 props 或 state 改變,React 會(huì)構(gòu)造一個(gè)新的虛擬 DOM 和舊的進(jìn)行對(duì)比來(lái)決定真實(shí) DOM 更新的必要性,只有在它們不相等的時(shí)候,React 才會(huì)使用盡量少的改動(dòng)更新 DOM。

在此之上,React 提供了生命周期函數(shù) shouldComponentUpdate,在重新渲染機(jī)制回路(虛擬 DOM 對(duì)比和 DOM 更新)之前會(huì)被觸發(fā),賦予開發(fā)者跳過這個(gè)過程的能力。這個(gè)函數(shù)默認(rèn)返回 true,讓 React 執(zhí)行更新。

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

一定要記住,React 會(huì)非常頻繁的調(diào)用這個(gè)函數(shù),所以要確保它的執(zhí)行速度夠快。

假如你有個(gè)帶有多個(gè)對(duì)話的消息應(yīng)用,如果只有一個(gè)對(duì)話發(fā)生改變,如果我們?cè)?ChatThread 組件執(zhí)行 shouldComponentUpdate,React 可以跳過其他對(duì)話的重新渲染步驟。

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

因此,總的說(shuō),React 通過讓用戶使用 shouldComponentUpdate 減短重新渲染回路,避免進(jìn)行昂貴的更新 DOM 子樹的操作,而且這些必要的更新,需要對(duì)比虛擬 DOM。

shouldComponentUpdate 實(shí)戰(zhàn)

這里有個(gè)組件的子樹,每一個(gè)都指明了 shouldComponentUpdate 返回值和虛擬 DOM 是否相等,最后,圓圈的顏色表示組件是否需要更新。

478926453-57adb1fb25515_articlex.png

在上面的示例中,因?yàn)?C2 的 shouldComponentUpdate 返回 false,React 就不需要生成新的虛擬 DOM,也就不需要更新 DOM,注意 React 甚至不需要調(diào)用 C4 和 C5 的 shouldComponentUpdate。

C1 和 C3 的 shouldComponentUpdate 返回 true,所以 React 需要向下到葉子節(jié)點(diǎn)檢查它們,C6 返回 true,因?yàn)樘摂M DOM 不相等,需要更新 DOM。最后感興趣的是 C8,對(duì)于這個(gè)節(jié)點(diǎn),React 需要計(jì)算虛擬 DOM,但是因?yàn)樗团f的相等,所以不需要更新 DOM。

注意 React 只需要對(duì) C6 進(jìn)行 DOM 轉(zhuǎn)換,這是必須的。對(duì)于 C8,通過虛擬 DOM 的對(duì)比確定它是不需要的,C2 的子樹和 C7,它們甚至不需要計(jì)算虛擬 DOM,因?yàn)?shouldComponentUpdate

那么,我們?cè)趺磳?shí)現(xiàn) shouldComponentUpdate 呢?比如說(shuō)你有一個(gè)組件僅僅渲染一個(gè)字符串:

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

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

我們可以簡(jiǎn)單的實(shí)現(xiàn) shouldComponentUpdate 如下:

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

非常好!處理這樣簡(jiǎn)單結(jié)構(gòu)的 props/state 很簡(jiǎn)單,我門甚至可以歸納出一個(gè)基于淺對(duì)比的實(shí)現(xiàn),然后把它 Mixin 到組件中。實(shí)際上 React 已經(jīng)提供了這樣的實(shí)現(xiàn): PureRenderMixin

但是如果你的組件的 props 或者 state 是可變的數(shù)據(jù)結(jié)構(gòu)呢?比如說(shuō),組件接收的 prop 不是一個(gè)像 'bar' 這樣的字符串,而是一個(gè)包涵字符串的 JavaScript 對(duì)象,比如 { foo: 'bar' }:

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

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

前面的 shouldComponentUpdate 實(shí)現(xiàn)就不會(huì)一直和我們期望的一樣工作:

// 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

這個(gè)問題是當(dāng) prop 沒有改變的時(shí)候 shouldComponentUpdate 也會(huì)返回 true。為了解決這個(gè)問題,我們有了這個(gè)替代實(shí)現(xiàn):

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

基本上,我們結(jié)束了使用深度對(duì)比來(lái)確保改變的正確跟蹤,這個(gè)方法在性能上的花費(fèi)是很大的,因?yàn)槲覀冃枰獮槊總€(gè) model 寫不同的深度對(duì)比代碼。就算這樣,如果我們沒有處理好對(duì)象引用,它甚至不能工作,比如說(shuō)這個(gè)父組件:

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>
    );
  }
});

內(nèi)部組件第一次渲染的時(shí)候,它會(huì)獲取 { foo: 'bar' } 作為 value 的值。如果用戶點(diǎn)擊了 a 標(biāo)簽,父組件的 state 會(huì)更新成 { value: { foo: 'barbar' } },觸發(fā)內(nèi)部組件的重新渲染過程,內(nèi)部組件會(huì)收到 { foo: 'barbar' } 作為 value 的新的值。

這里的問題是因?yàn)楦附M件和內(nèi)部組件共享同一個(gè)對(duì)象的引用,當(dāng)對(duì)象在 onClick 函數(shù)的第二行發(fā)生改變的時(shí)候,內(nèi)部組件的屬性也發(fā)生了改變,所以當(dāng)重新渲染過程開始,shouldComponentUpdate 被調(diào)用的時(shí)候,this.props.value.foonextProps.value.foo 是相等的,因?yàn)閷?shí)際上 this.props.valuenextProps.value 是同一個(gè)對(duì)象的引用。

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

Immutable-js 來(lái)救贖

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

  • Immutable: 一旦創(chuàng)建,集合就不能再改變。

  • Persistent: 新的集合類型可以通過之前的集合創(chuàng)建,比如 set 產(chǎn)生改變的集合。創(chuàng)建新的集合之后源集合仍然有效。

  • Structural Sharing: 新的集合會(huì)使用盡量多的源集合的結(jié)構(gòu),減少?gòu)?fù)制來(lái)節(jié)省空間和性能友好。如果新的集合和源集合相等,一般會(huì)返回源結(jié)構(gòu)。

不可變讓跟蹤改變非常簡(jiǎn)單;每次改變都是產(chǎn)生新的對(duì)象,所以我們僅需要對(duì)象的引用是否改變,比如這段簡(jiǎn)單的 JavaScript 代碼:

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

盡管 y 被改變,因?yàn)樗?x 引用的是同一個(gè)對(duì)象,這個(gè)對(duì)比返回 true。然而,這個(gè)代碼可以使用 immutable-js 改寫如下:

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

這個(gè)例子中,因?yàn)楦淖?x 的時(shí)候返回了新的引用,我們就可以安全的認(rèn)為 x 已經(jīng)改變。

臟檢測(cè)可以作為另外的可行的方式追蹤改變,給 setters 一個(gè)標(biāo)示。這個(gè)方法的問題是,它強(qiáng)制你使用 setters,而且要寫很多額外的代碼,影響你的類?;蛘吣憧梢栽诟淖冎吧羁截悓?duì)象,然后進(jìn)行深對(duì)比來(lái)確定是不是發(fā)生了改變。這個(gè)方法的問題是,深拷貝和深對(duì)比都是很花性能的操作。

因此,不可變數(shù)據(jù)結(jié)構(gòu)給你提供了一個(gè)高效、簡(jiǎn)潔的方式來(lái)跟蹤對(duì)象的改變,而跟蹤改變是實(shí)現(xiàn) shouldComponentUpdate 的關(guān)鍵。所以,如果我們使用 immutable-js 提供的抽象創(chuàng)建 props 和 state 模型,我們就可以使用 PureRenderMixin,而且能夠獲得很好的性能增強(qiáng)。

Immutable-js 和 Flux

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

讓我們看一個(gè)可行的方式,使用不可變數(shù)據(jù)結(jié)構(gòu)來(lái)給消息示例創(chuàng)建數(shù)據(jù)結(jié)構(gòu)。首先我們要給每個(gè)要建模的實(shí)體定義一個(gè) Record。Records 僅僅是一個(gè)不可變?nèi)萜鳎锩姹4嬉幌盗芯唧w數(shù)據(jù):

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

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

Record 方法接收一個(gè)對(duì)象,來(lái)定義字段和對(duì)應(yīng)的默認(rèn)數(shù)據(jù)。

消息的 store 可以使用兩個(gè) list 來(lái)跟蹤 users 和 messages:

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

實(shí)現(xiàn)函數(shù)處理每個(gè) payload 類型應(yīng)該是比較簡(jiǎn)單的,比如,當(dāng) store 看到一個(gè)代表新消息的 payload 時(shí),我們就創(chuàng)建一個(gè)新的 record,并放入消息列表:

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

注意:因?yàn)閿?shù)據(jù)結(jié)構(gòu)不可變,我們需要把 push 方法的結(jié)果賦給 this.messages。

在 React 里,如果我們也使用 immutable-js 數(shù)據(jù)結(jié)構(gòu)來(lái)保存組件的 state,我門可以把 PureRenderMixin 混入到我門所有的組件來(lái)縮短重新渲染回路。

這篇文章是翻譯React官方文檔

https://segmentfault.com/a/1190000006254212

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容