React優化性能的經驗教訓

1. 基本原理

image.png

向更大的世界邁出第一步。

1.1 render()函數

一般來說,要盡可能少地在 render 函數中做操作。如果非要做一些復雜操作或者計算,也許你可以考慮使用一個 memoized 函數以便于緩存那些重復的結果。可以看看 Lodash.memoize,這是一個開箱即用的記憶函數。

反過來講,避免在組件的 state 上存儲一些容易計算的值也很重要。舉個例子,如果 props 同時包含 firstName 和 lastName,沒必要在 state 上存一個 fullName,因為它可以很容易通過提供的 props 來獲取。如果一個值可以通過簡單的字符串拼接或基本的算數運算從 props 派生出來,那么沒理由將這些值包含在組件的 state 上。

1.2 Prop 和 Reconciliation

重要的是要記住,只要 props(或 state)的值不等于之前的值,React 就會觸發重新渲染。如果 props 或者 state 包含一個對象或者數組,嵌套值中的改變會觸發重新渲染。考慮到這一點,你需要注意在每次渲染的生命周期中,創建一個新的 props 或者 state 都可能無意中導致了性能下降。(注:對象或者數組只要引用不變,是不會觸發rerender的)

例子: 函數綁定的問題

/*
給 prop 傳入一個行內綁定的函數(包括 ES6 箭頭函數)實質上是在每次父組件 render 時傳入一個新的函數。
*/
render() {
  return (
    <div>
      <a onClick={ () => this.doSomething() }>Bad</a>
      <a onClick={ this.doSomething.bind( this ) }>Bad</a>
    </div>
  );
}


/*
應該在構造函數中處理函數綁定并且將已經綁定好的函數作為 prop 的值
*/

constructor( props ) {
  this.doSomething = this.doSomething.bind( this );
  //or
  this.doSomething = (...args) => this.doSomething(...args);
}
render() {
  return (
    <div>
      <a onClick={ this.doSomething }>Good</a>
    </div>
  );
}

例子: 對象或數組字面量

/*
對象或者數組字面量在功能上來看是調用了 Object.create() 和 new Array()。這意味如果給 prop 傳遞了對象字面量或者數組字面量。每次render 時 React 會將他們作為一個新的值。這在處理 Radium 或者行內樣式時通常是有問題的。
*/

/* Bad */
// 每次渲染時都會為 style 新建一個對象字面量
render() {
  return <div style={ { backgroundColor: 'red' } }/>
}

/* Good */
// 在組件外聲明
const style = { backgroundColor: 'red' };

render() {
  return <div style={ style }/>
}

例子 : 注意兜底值字面量

/*
有時我們會在 render 函數中創建一個兜底的值來避免 undefined 報錯。在這些情況下,最好在組件外創建一個兜底的常量而不是創建一個新的字面量。
/*
/* Bad */
render() {
  let thingys = [];
  // 如果 this.props.thingys 沒有被定義,一個新的數組字面量會被創建
  if( this.props.thingys ) {
    thingys = this.props.thingys;
  }

  return <ThingyHandler thingys={ thingys }/>
}

/* Bad */
render() {
  // 這在功能上和前一個例子一樣
  return <ThingyHandler thingys={ this.props.thingys || [] }/>
}

/* Good */

// 在組件外部聲明
const NO_THINGYS = [];

render() {
  return <ThingyHandler thingys={ this.props.thingys || NO_THINGYS }/>
}

1.3 盡可能的保持 Props(和 State)簡單和精簡

理想情況下,傳遞給組件的 props 應該是它直接需要的。為了將值傳給子組件而將一個大的、復雜的對象或者很多獨立的 props 傳遞給一個組件會導致很多不必要的組件渲染(并且會增加開發復雜性)。

我們使用 Redux 作為狀態容器,所以在我們看來,最理想的是方案在組件層次結構的每一個層級中使用 react-reduxconnect() 函數直接從 store 上獲取數據。connect 函數的性能很好,并且使用它的開銷也非常小。

1.4 組件方法

由于組件方法是為組件的每個實例創建的,如果可能的話,使用 helper/util 模塊的純函數或者靜態類方法。尤其在渲染大量組件的應用中會有明顯的區別。

2. 進階

image.png

視圖的變化是邪惡的

2.1 shouldComponentUpdate()

React 有一個生命周期函數 shouldComponentUpdate()。這個方法可以根據當前的和下一次的 props 和 state 來通知這個 React 組件是否應該被重新渲染。

然而使用這個方法有一個問題,開發者必須考慮到需要觸發重新渲染的每一種情況。這會導致邏輯復雜,一般來說,會非常痛苦。如果非常需要,你可以使用一個自定義的shouldComponentUpdate()
方法,但是很多情況下有更好的選擇。

2.2 React.PureComponent

React 從 v15 開始會包含一個 PureComponent 類,它可以被用來構建組件。React.PureComponent聲明了它自己的 shouldComponentUpdate() 方法,它自動對當前的和下一次的 props 和 state 做一次淺對比。有關淺對比的更多信息,請參考這個 Stack Overflow:http://stackoverflow.com/questions/36084515/how-does-shallow-compare-work-in-react

在大多數情況下,React.PureComponent 是比 React.Component更好的選擇。在創建新組件時,首先嘗試將其構建為純組件,只有組件的功能需要時才使用 React.Component。更多信息,請查閱相關文檔 React.PureComponent

2.3 組件性能分析(在 Chrome 里)

在新版本的 Chrome 里,timeline 工具里有一個額外的內置功能可以顯示哪些 React 組件正在渲染以及他們花費的時間。要啟用此功能,將 ?react_perf
作為要測試的 URL 的查詢字符串。React 渲染時間軸數據將位于 User Timing 部分。
更多相關信息,請查閱官方文檔:Profiling Components with Chrome Timeline

2.4 有用的工具: why-did-you-update

這是一個很棒的 NPM 包,他們給 React 添加補丁,當一個組件觸發了不必要的重新渲染時,它會在控制臺輸出一個 console 提示。

注意: 這個模塊在初始化時可以通過一個過濾器匹配特定的想要優化的組件,否則你的命令行可能會被垃圾信息填滿,并且可能你的瀏覽器會掛起或者崩潰,查閱 why-did-you-update 文檔獲取更多詳細信息。

3. 常見性能陷阱

image.png

3.1 setTimeout() 和 setInterval()

在 React 組件中使用 setTimeout() 或者 setInterval() 要十分小心。幾乎總是有更好的選擇,例如 'resize' 和 'scroll' 事件(注意:有關注意事項請參閱下一節)。

如果你需要使用 setTimeout() 和 setInterval(),你必須遵守下面兩條建議

不要設置過短的時間間隔。

當心那些小于 100 ms 的定時器,他們很可能是沒意義的。如果確實需要一個更短的時間,可以使用 window.requestAnimationFrame()

保留對這些函數的引用,并且在 unmount 時取消或者銷毀他們。

setTimeout() 和 setInterval() 都返回一個延遲函數的引用,并且需要的時候可以取消它們。由于這些函數是在全局作用域執行的,他們不在乎你的組件是否存在,這會導致報錯甚至程序卡死。

注意: 對 window.requestAnimationFrame() 來說也是如此

解決這個問題最簡答的方法是使用 react-timeout 這個 NPM 包,它提供了一個可以自動處理上述內容的高階組件。它將 setTimeout/setInterval 等功能添加到包裝組建的 props 上。(特別感謝 Vixlet 的開發人員 Carl Pillot 提供這個方法)
如果你不想引入這個依賴,并且希望自行解決此問題,你可以使用以下的方法:

// 如何正確取消 timeouts/intervals

compnentDidMount() {
 this._timeoutId = setTimeout( this.doFutureStuff, 1000 );
 this._intervalId = setInterval( this.doStuffRepeatedly, 5000 );
}
componentWillUnmount() {
 /*
   高級提示:如果操作已經完成,或者值未被定義,這些函數也不會報錯
 */
 clearTimeout( this._timeoutId );
 clearInterval( this._intervalId );
}

如果你使用 requestAnimationFrame() 執行的一個動畫循環,可以使用一個非常相似的解決方案,當前代碼要有一點小的修改:

// 如何確保我們的動畫循環在組件消除時結束

componentDidMount() {
  this.startLoop();
}

componentWillUnmount() {
  this.stopLoop();
}

startLoop() {
  if( !this._frameId ) {
    this._frameId = window.requestAnimationFrame( this.loop );
  }
}

loop() {
  // 在這里執行循環工作
  this.theoreticalComponentAnimationFunction()

  // 設置循環的下一次迭代
  this.frameId = window.requestAnimationFrame( this.loop )
}

stopLoop() {
  window.cancelAnimationFrame( this._frameId );
  // 注意: 不用擔心循環已經被取消
  // cancelAnimationFrame() 不會拋出異常
}

3.2 未去抖頻繁觸發的事件

某些常見的事件可能會非常頻繁的觸發,例如 scroll,resize
。去抖這些事件是明智的,特別是如果事件處理程序執行的不僅僅是基本功能。Lodash 有 _.debounce 方法。在 NPM 上還有一個獨立的 debounce 包.
“但是我真的需要立即反饋 scroll/resize 或者別的事件”

我發現一種可以處理這些事件并且以高性能的方式進行響應的方法,那就是在第一次事件觸發時啟動 requestAnimationFrame() 循環。然后可以使用 debounce() 方法并且將 trailing 這個配置項設為 true
這意味著該功能只在頻繁觸發的事件流結束后觸發)來取消對值的監聽,看看下面這個例子。

class ScrollMonitor extends React.Component {
  constructor() {
    this.handleScrollStart = this.startWatching.bind( this );
    this.handleScrollEnd = debounce(
      this.stopWatching.bind( this ),
      100,
      { leading: false, trailing: true } );
  }

  componentDidMount() {
    window.addEventListener( 'scroll', this.handleScrollStart );
    window.addEventListener( 'scroll', this.handleScrollEnd );
  }

  componentWillUnmount() {
    window.removeEventListener( 'scroll', this.handleScrollStart );
    window.removeEventListener( 'scroll', this.handleScrollEnd );

    //確保組件銷毀后結束循環
    this.stopWatching();
  }

  // 如果循環未開始,啟動它
  startWatching() {
    if( !this._watchFrame ) {
      this.watchLoop();
    }
  }

  // 取消下一次迭代
  stopWatching() {
    window.cancelAnimationFrame( this._watchFrame );
  }

  // 保持動畫的執行直到結束
  watchLoop() {
    this.doThingYouWantToWatchForExampleScrollPositionOrWhatever()

    this._watchFrame = window.requestAnimationFrame( this.watchLoop )
  }

}

3.3 密集CPU任務線程阻塞

某些任務一直是 CPU 密集型的,因此可能會導致主渲染線程的阻塞。舉幾個例子,比如非常復雜的數學計算,迭代非常大的數組,使用 File api 進行文件讀寫,利用 <canvas> 對圖片進行編碼解碼。

在這些情況下,如果有可能最好使用 Web Worker 將這些功能移到另一個線程上,這樣我們的主渲染線程可以保持順滑。

歡迎關注極客教程微信公眾號平臺:geekjc

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容