React純組件渲染性能反模式
React純組件的渲染可以非常高效,但是需要用戶將其數(shù)據(jù)作為不可變的對(duì)象,才能正常工作。但是由于JavaScript的原因,有時(shí)做到這點(diǎn)可能非常具有挑戰(zhàn)性。
反模式是在
Render函數(shù)或者Redux的connect(mapState)中創(chuàng)建新的數(shù)組、對(duì)象、函數(shù)或者其他新的對(duì)象
純渲染?
說起React的純渲染,我指的是組件應(yīng)該通過淺比較來實(shí)現(xiàn)shouldComponentUpdate方法。例如PureRenderMixin,recompose/pure ,等等
為什么?
這可能是你能在React中做的最顯著的一個(gè)性能優(yōu)化。這也是ClojureScript默認(rèn)對(duì)React所做的封裝,并且聲稱其比普通的React速度快的原因。因?yàn)镃lojureScript必須使用不變的數(shù)據(jù)結(jié)構(gòu)來存儲(chǔ)state,所以在判斷是否重新渲染組件時(shí)花費(fèi)很小。然而使用可變的數(shù)據(jù)來深比較數(shù)據(jù)是否相等將非常耗費(fèi)性能。在ClojureScript中,這很簡(jiǎn)單,因?yàn)樗械膶?duì)象總是不可變的,但是在Javascript中不是如此。
公平的說,即使沒有使用純渲染優(yōu)化,React也會(huì)很快,但是當(dāng)使用基于JavaScript的動(dòng)畫(例如react-motion),在1s內(nèi)組件會(huì)被成千上萬次渲染,或者使用大的組件,例如有上千個(gè)單元格的可編輯的表格,在這些情況下,優(yōu)化將變得至關(guān)重要。同樣在低配置的移動(dòng)設(shè)備中,你會(huì)從中得到巨大的性能提升。
反模式
幾個(gè)月之前,我寫了一個(gè)可編輯的表格,用來從電子表格中導(dǎo)入用戶數(shù)據(jù)。一張表格很容易就有超過500個(gè)用戶。在最上層的組件中,我寫的代碼如下:
class Table extends PureComponent {
render() {
return (
<div>
{this.props.items.map(i =>
<Cell data={i} options={this.props.options || []} />
)}
</div>
);
}
}
實(shí)際上,代碼比這更加復(fù)雜。Cell組件非常復(fù)雜,對(duì)于每個(gè)用戶渲染了好多的單元格。所以在應(yīng)用中有上千個(gè)Cell元素。
在應(yīng)用中我載入了500個(gè)用戶,并且嘗試修改一個(gè)單元格,修改的動(dòng)作在我高性能的電腦上竟然花費(fèi)了幾秒時(shí)間才完成!后來使用了console.log()來調(diào)試代碼后,我發(fā)現(xiàn)當(dāng)一個(gè)很小的單元格改變后,幾乎整個(gè)應(yīng)用都會(huì)被重新渲染。這怎么可能?我使用的是Redux,凍結(jié)了應(yīng)用的狀態(tài),并且使用了不可變的數(shù)據(jù)。
經(jīng)過幾個(gè)小時(shí)抓破頭皮的思考,我意識(shí)到,這其中的一個(gè)改變時(shí)我使用的數(shù)組的默認(rèn)值:
this.props.options || []
可以看到options數(shù)組傳遞給Cell元素。通常來說,這沒有任何問題。其他的Cell元素也不會(huì)被渲染,因?yàn)樗麄兛梢宰鰷\比較來檢查屬性是否一致,并且在一致時(shí)跳過渲染,但是萬一props是null,就會(huì)使用默認(rèn)的數(shù)組。正如你所知道的那樣,數(shù)組字面量和new Array()都會(huì)創(chuàng)建一個(gè)新的數(shù)組實(shí)例。這會(huì)徹底的破壞掉Cell元素內(nèi)純組件渲染優(yōu)化。Javascript的不同實(shí)例是不相等的,淺比較是否相等總是會(huì)返回false,并且告訴React來重新渲染組件。修改的方法非常簡(jiǎn)單:
const default = [];
class Table extends PureComponent {
render() {
return (
<div>
{this.props.items.map(i =>
<Cell data={i} options={this.props.options || default} />
)}
</div>
);
}
}
現(xiàn)在修改操作只需要幾十毫秒!并且defaultProps的作用和以前一樣。
函數(shù)也會(huì)創(chuàng)建新對(duì)象
在render中創(chuàng)建函數(shù)也會(huì)有同樣的問題,好多代碼是如下這樣寫的:
class App extends PureComponent {
render() {
return <MyInput
onChange={e => this.props.update(e.target.value)} />;
}
}
或者
class App extends PureComponent {
update(e) {
this.props.update(e.target.value);
}
render() {
return <MyInput onChange={this.update.bind(this)} />;
}
}
和上面的數(shù)組字面量類似,在這兩種情況下,都會(huì)創(chuàng)建一個(gè)新的函數(shù)對(duì)象。你應(yīng)該盡早的執(zhí)行綁定this:
class App extends PureComponent {
constructor(props) {
super(props);
this.update = this.update.bind(this);
}
update(e) {
this.props.update(e.target.value);
}
render() {
return <MyInput onChange={this.update} />;
}
}
還需要重復(fù)一點(diǎn)。也有其他的方法來解決這個(gè)問題,使用React.createClass()來自動(dòng)綁定所有的方法或者使用Babel來箭頭函數(shù),還有使用自動(dòng)綁定裝飾器。
ESLint rule react/jsx-no-bind 是一個(gè)用來捕獲該問題的工具。
在Reducconnect(mapState)中使用Reselect
起初,我并不認(rèn)為Reselect(一個(gè)在Redux官方文檔中提到的類庫)會(huì)如此重要,因?yàn)槲液苌僭赗eduxconnect()方法中寫性能低下的map state函數(shù)。我錯(cuò)的是如此離譜。這和函數(shù)的性能沒有關(guān)系,關(guān)鍵是新對(duì)象(吃驚吧?。?,考慮如下的map state函數(shù):
let App = ({otherData, resolution}) => (
<div>
<DataContainer data={otherData} />
<ResolutionContainer resolution={resolution} />
</div>
);
const doubleRes = (size) => ({
width: size.width*2,
height: size.height*2
});
App = connect(state => {
return {
otherData: state.otherData,
resolution: doubleRes(state.resolution)
}
})(App);
在這個(gè)例子中,state中otherData每次發(fā)生變化,DataContainer和ResolutionContainer都會(huì)重新渲染,即使state中的resolution沒有發(fā)生變化。這是因?yàn)楹瘮?shù)doubleRes總是會(huì)返回一個(gè)新的resolution對(duì)象。如果使用Reselect重寫doubleRes,問題就會(huì)變?yōu)槿缦碌那闆r:
import {createSelector} from “reselect”;
const doubleRes = createSelector(
r => r.width,
r => r.height,
(width, height) => ({
width: width*2,
height: heiht*2
})
);
Reselect會(huì)記住上一次函數(shù)的結(jié)果,在傳入?yún)?shù)沒有改變的情況下,將其返回。
結(jié)論
當(dāng)你注意到的時(shí)候,反模式很明顯,但是仍然很容易陷進(jìn)去。比較好的方面是,如果你搞砸了,就像我之前那樣,這不會(huì)破壞你的應(yīng)用,只是會(huì)運(yùn)行的比較慢一點(diǎn),大多數(shù)情況下,并不重要。但是我希望在這篇文章中給你指向了應(yīng)該去深入研究的某些內(nèi)容。