React純組件渲染性能反模式

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ā)生變化,DataContainerResolutionContainer都會(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)容。

翻譯自React.js pure render performance anti-pattern

最后編輯于
?著作權(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)容

  • 本筆記基于React官方文檔,當(dāng)前React版本號(hào)為15.4.0。 1. 安裝 1.1 嘗試 開始之前可以先去co...
    Awey閱讀 7,934評(píng)論 14 128
  • 最近看了一本關(guān)于學(xué)習(xí)方法論的書,強(qiáng)調(diào)了記筆記和堅(jiān)持的重要性。這幾天也剛好在學(xué)習(xí)React,所以我打算每天堅(jiān)持一篇R...
    gaoer1938閱讀 1,818評(píng)論 0 5
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,323評(píng)論 25 708
  • 伍天友閱讀 167評(píng)論 0 0
  • 思考:歸納法,是從特殊歸納出一般的方法。用在工作中,比如, 根據(jù)消費(fèi)者購(gòu)物的習(xí)慣: 1. 總結(jié)出賣場(chǎng)陳列黃金區(qū)及盲...
    楊雪雪閱讀 247評(píng)論 0 0

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