React性能優(yōu)化
hepeguo 2016年08月12日發(fā)布
當(dāng)大家考慮在項目中使用 React 的時候旺隙,第一個問題往往是他們的應(yīng)用的速度和響應(yīng)是否能和非 React 版一樣,每當(dāng)狀態(tài)改變的時候就重新渲染組件的整個子樹昭躺,讓大家懷疑這會不會對性能造成負面影響则果。React 用了一些黑科技來減少 UI 更新需要的花費較大的 DOM 操作幔翰。
使用 production 版本
如果你在你的 React app 中進行性能測試或在尋找性能問題,一定要確定你在使用 minified production build西壮。開發(fā)者版本包括額外的警告信息遗增,這對你在開發(fā)你的 app 的時候很有用,但是因為要進行額外的處理款青,所以它也會比較慢做修。
避免更新 DOM
React 使用虛擬 DOM,它是在瀏覽器中的 DOM 子樹的渲染描述抡草,這個平行的描述讓 React 避免創(chuàng)建和操作 DOM 節(jié)點饰及,這些遠比操作一個 JavaScript 對象慢。當(dāng)一個組件的 props 或 state 改變渠牲,React 會構(gòu)造一個新的虛擬 DOM 和舊的進行對比來決定真實 DOM 更新的必要性旋炒,只有在它們不相等的時候,React 才會使用盡量少的改動更新 DOM签杈。
在此之上瘫镇,React 提供了生命周期函數(shù) shouldComponentUpdate
,在重新渲染機制回路(虛擬 DOM 對比和 DOM 更新)之前會被觸發(fā)答姥,賦予開發(fā)者跳過這個過程的能力铣除。這個函數(shù)默認(rèn)返回 true
,讓 React 執(zhí)行更新鹦付。
shouldComponentUpdate: function(nextProps, nextState) {
return true;
}
一定要記住尚粘,React 會非常頻繁的調(diào)用這個函數(shù),所以要確保它的執(zhí)行速度夠快敲长。
假如你有個帶有多個對話的消息應(yīng)用郎嫁,如果只有一個對話發(fā)生改變秉继,如果我們在 ChatThread
組件執(zhí)行 shouldComponentUpdate
,React 可以跳過其他對話的重新渲染步驟泽铛。
shouldComponentUpdate: function(nextProps, nextState) {
// TODO: return whether or not current chat thread is
// different to former one.
}
因此尚辑,總的說,React 通過讓用戶使用 shouldComponentUpdate
減短重新渲染回路盔腔,避免進行昂貴的更新 DOM 子樹的操作杠茬,而且這些必要的更新,需要對比虛擬 DOM弛随。
shouldComponentUpdate 實戰(zhàn)
這里有個組件的子樹瓢喉,每一個都指明了 shouldComponentUpdate
返回值和虛擬 DOM 是否相等,最后舀透,圓圈的顏色表示組件是否需要更新栓票。
在上面的示例中,因為 C2 的 shouldComponentUpdate
返回 false盐杂,React 就不需要生成新的虛擬 DOM逗载,也就不需要更新 DOM,注意 React 甚至不需要調(diào)用 C4 和 C5 的 shouldComponentUpdate
链烈。
C1 和 C3 的 shouldComponentUpdate
返回 true
厉斟,所以 React 需要向下到葉子節(jié)點檢查它們,C6 返回 true
强衡,因為虛擬 DOM 不相等擦秽,需要更新 DOM。最后感興趣的是 C8漩勤,對于這個節(jié)點感挥,React 需要計算虛擬 DOM,但是因為它和舊的相等越败,所以不需要更新 DOM触幼。
注意 React 只需要對 C6 進行 DOM 轉(zhuǎn)換,這是必須的究飞。對于 C8置谦,通過虛擬 DOM 的對比確定它是不需要的,C2 的子樹和 C7亿傅,它們甚至不需要計算虛擬 DOM媒峡,因為 shouldComponentUpdate
。
那么葵擎,我們怎么實現(xiàn) shouldComponentUpdate
呢谅阿?比如說你有一個組件僅僅渲染一個字符串:
React.createClass({
propTypes: {
value: React.PropTypes.string.isRequired
},
render: function() {
return <div>{this.props.value}</div>;
}
});
我們可以簡單的實現(xiàn) shouldComponentUpdate
如下:
shouldComponentUpdate: function(nextProps, nextState) {
return this.props.value !== nextProps.value;
}
非常好!處理這樣簡單結(jié)構(gòu)的 props/state 很簡單,我門甚至可以歸納出一個基于淺對比的實現(xiàn)签餐,然后把它 Mixin 到組件中寓涨。實際上 React 已經(jīng)提供了這樣的實現(xiàn): PureRenderMixin
但是如果你的組件的 props 或者 state 是可變的數(shù)據(jù)結(jié)構(gòu)呢?比如說氯檐,組件接收的 prop 不是一個像 'bar'
這樣的字符串缅茉,而是一個包涵字符串的 JavaScript 對象,比如 { foo: 'bar' }
:
React.createClass({
propTypes: {
value: React.PropTypes.object.isRequired
},
render: function() {
return <div>{this.props.value.foo}</div>;
}
});
前面的 shouldComponentUpdate
實現(xiàn)就不會一直和我們期望的一樣工作:
// 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
這個問題是當(dāng) prop 沒有改變的時候 shouldComponentUpdate
也會返回 true
男摧。為了解決這個問題,我們有了這個替代實現(xiàn):
shouldComponentUpdate: function(nextProps, nextState) {
return this.props.value.foo !== nextProps.value.foo;
}
基本上译打,我們結(jié)束了使用深度對比來確保改變的正確跟蹤耗拓,這個方法在性能上的花費是很大的,因為我們需要為每個 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>
);
}
});
內(nèi)部組件第一次渲染的時候竿刁,它會獲取 { foo: 'bar' }
作為 value 的值。如果用戶點擊了 a 標(biāo)簽搪缨,父組件的 state 會更新成 { value: { foo: 'barbar' } }
食拜,觸發(fā)內(nèi)部組件的重新渲染過程,內(nèi)部組件會收到 { foo: 'barbar' }
作為 value 的新的值副编。
這里的問題是因為父組件和內(nèi)部組件共享同一個對象的引用负甸,當(dāng)對象在 onClick
函數(shù)的第二行發(fā)生改變的時候,內(nèi)部組件的屬性也發(fā)生了改變痹届,所以當(dāng)重新渲染過程開始呻待,shouldComponentUpdate
被調(diào)用的時候,this.props.value.foo
和 nextProps.value.foo
是相等的队腐,因為實際上 this.props.value
和 nextProps.value
是同一個對象的引用蚕捉。
因此,我們會丟失 prop 的改變柴淘,縮短重新渲染過程迫淹,UI 也不會從 'bar'
更新到 'barbar'
Immutable-js 來救贖
Immutable-js 是 Lee Byron 寫的 JavaScript 集合類型的庫,最近被 Facebook 開源悠就,它通過結(jié)構(gòu)共享提供不可變持久化集合類型千绪。一起看下這些特性的含義:
Immutable: 一旦創(chuàng)建,集合就不能再改變梗脾。
Persistent: 新的集合類型可以通過之前的集合創(chuàng)建荸型,比如 set 產(chǎn)生改變的集合。創(chuàng)建新的集合之后源集合仍然有效。
Structural Sharing: 新的集合會使用盡量多的源集合的結(jié)構(gòu)瑞妇,減少復(fù)制來節(jié)省空間和性能友好稿静。如果新的集合和源集合相等,一般會返回源結(jié)構(gòu)辕狰。
不可變讓跟蹤改變非常簡單改备;每次改變都是產(chǎn)生新的對象,所以我們僅需要對象的引用是否改變蔓倍,比如這段簡單的 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
的時候返回了新的引用聚谁,我們就可以安全的認(rèn)為 x
已經(jīng)改變母剥。
臟檢測可以作為另外的可行的方式追蹤改變,給 setters 一個標(biāo)示形导。這個方法的問題是环疼,它強制你使用 setters,而且要寫很多額外的代碼朵耕,影響你的類炫隶。或者你可以在改變之前深拷貝對象阎曹,然后進行深對比來確定是不是發(fā)生了改變等限。這個方法的問題是,深拷貝和深對比都是很花性能的操作芬膝。
因此望门,不可變數(shù)據(jù)結(jié)構(gòu)給你提供了一個高效、簡潔的方式來跟蹤對象的改變锰霜,而跟蹤改變是實現(xiàn) shouldComponentUpdate
的關(guān)鍵筹误。所以,如果我們使用 immutable-js 提供的抽象創(chuàng)建 props 和 state 模型癣缅,我們就可以使用 PureRenderMixin
厨剪,而且能夠獲得很好的性能增強。
Immutable-js 和 Flux
如果你在使用 Flux友存,你應(yīng)該開始使用 immutable-js 寫你的 stores祷膳,看一下 full API。
讓我們看一個可行的方式屡立,使用不可變數(shù)據(jù)結(jié)構(gòu)來給消息示例創(chuàng)建數(shù)據(jù)結(jié)構(gòu)直晨。首先我們要給每個要建模的實體定義一個 Record
。Records 僅僅是一個不可變?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
方法接收一個對象勇皇,來定義字段和對應(yīng)的默認(rèn)數(shù)據(jù)罩句。
消息的 store 可以使用兩個 list 來跟蹤 users 和 messages:
this.users = Immutable.List();
this.messages = Immutable.List();
實現(xiàn)函數(shù)處理每個 payload 類型應(yīng)該是比較簡單的,比如敛摘,當(dāng) store 看到一個代表新消息的 payload 時门烂,我們就創(chuàng)建一個新的 record,并放入消息列表:
this.messages = this.messages.push(new Message({
timestamp: payload.timestamp,
sender: payload.sender,
text: payload.text
});
注意:因為數(shù)據(jù)結(jié)構(gòu)不可變兄淫,我們需要把 push 方法的結(jié)果賦給 this.messages
屯远。
在 React 里,如果我們也使用 immutable-js 數(shù)據(jù)結(jié)構(gòu)來保存組件的 state捕虽,我門可以把 PureRenderMixin
混入到我門所有的組件來縮短重新渲染回路氓润。
這篇文章是翻譯React官方文檔