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 是否相等,最后,圓圈的顏色表示組件是否需要更新。

在上面的示例中,因?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.foo 和 nextProps.value.foo 是相等的,因?yàn)閷?shí)際上 this.props.value 和 nextProps.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官方文檔