【譯】光速 React

光速 React

Vixlet 團隊優化性能的經驗教訓

在過去一年多,咱們 Vixlet 的 web 團隊已經着手於一個激動人心的項目:將咱們的整個 web 應用遷移到 React + Redux 架構。對於整個團隊來講,這是不斷增加的機遇,而在遷移過程當中,咱們一路風雨兼程。javascript

由於咱們的 web-app 可能有很是大的 feed 視圖,包括成百上千的媒體、文本、視頻、連接元素,咱們花了至關多的時間尋找能充分利用 React 性能的方法。在這裏,咱們將分享咱們這一路學到的一些經驗教訓。html

聲明下面講的作法和方法更適用於咱們具體應用的性能需求。然而,像全部的開發者建議的那樣,最重要的是要考慮到你的應用程序和團隊的實際需求。React 是一個開箱即用的框架,因此你可能不須要像咱們同樣細緻地優化性能。話雖如此,咱們仍是但願你能在這篇文章裏找到一些有用的信息。前端

基本原理

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

render() 函數

通常來講,要儘量少地在 render 函數中作操做。若是非要作一些複雜操做或者計算,也許你能夠考慮使用一個 memoized 函數以便於緩存那些重複的結果。能夠看看 Lodash.memoize,這是一個開箱即用的記憶函數。react

反過來說,避免在組件的 state 上存儲一些容易計算的值也很重要。舉個例子,若是 props 同時包含 firstNamelastName,不必在 state 上存一個 fullName,由於它能夠很容易經過提供的 props 來獲取。若是一個值能夠經過簡單的字符串拼接或基本的算數運算從 props 派生出來,那麼沒理由將這些值包含在組件的 state 上。android

Prop 和 Reconciliation

重要的是要記住,只要 props(或 state)的值不等於以前的值,React 就會觸發從新渲染。若是 props 或者 state 包含一個對象或者數組,嵌套值中的任何改變也會觸發從新渲染。考慮到這一點,你須要注意在每次渲染的生命週期中,建立一個新的 props 或者 state 均可能無心中致使了性能降低。
PS:譯者對這段保留意見,對象或者數組只要引用不變,是不會觸發rerender的,是我翻譯有誤仍是原文的錯誤?ios

例子: 函數綁定的問題git

/* 給 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>
  );
}複製代碼

例子: 對象或數組字面量github

/* 對象或者數組字面量在功能上來看是調用了 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 }/>
}複製代碼

例子 : 注意兜底值字面量web

/* 有時咱們會在 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 }/>
}複製代碼

儘量的保持 Props(和 State)簡單和精簡

理想狀況下,傳遞給組件的 props 應該是它直接須要的。爲了將值傳給子組件而將一個大的、複雜的對象或者不少獨立的 props 傳遞給一個組件會致使不少沒必要要的組件渲染(而且會增長開發複雜性)。

在 Vixlet,咱們使用 Redux 做爲狀態容器,因此在咱們看來,最理想的是方案在組件層次結構的每個層級中使用 react-reduxconnect() 函數直接從 store 上獲取數據。connect 函數的性能很好,而且使用它的開銷也很是小。

組件方法

因爲組件方法是爲組件的每一個實例建立的,若是可能的話,使用 helper/util 模塊的純函數或者靜態類方法。尤爲在渲染大量組件的應用中會有明顯的區別。

進階

在我看來視圖的變化是邪惡的!

shouldComponentUpdate()

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

然而使用這個方法有一個問題,開發者必須考慮到須要觸發從新渲染的每一種狀況。這會致使邏輯複雜,通常來講,會很是痛苦。若是很是須要,你可使用一個自定義的 shouldComponentUpdate() 方法,可是不少狀況下有更好的選擇。

React.PureComponent

React 從 v15 開始會包含一個 PureComponent 類,它能夠被用來構建組件。React.PureComponent 聲明瞭它本身的 shouldComponentUpdate() 方法,它自動對當前的和下一次的 props 和 state 作一次淺對比。有關淺對比的更多信息,請參考這個 Stack Overflow:

stackoverflow.com/questions/3…

在大多數狀況下,React.PureComponent 是比 React.Component 更好的選擇。在建立新組件時,首先嚐試將其構建爲純組件,只有組件的功能須要時才使用 React.Component

更多信息,請查閱相關文檔 React.PureComponent

組件性能分析(在 Chrome 裏)

在新版本的 Chrome 裏,timeline 工具裏有一個額外的內置功能能夠顯示哪些 React 組件正在渲染以及他們花費的時間。要啓用此功能,將 ?react_perf 做爲要測試的 URL 的查詢字符串。React 渲染時間軸數據將位於 User Timing 部分。

更多相關信息,請查閱官方文檔:Profiling Components with Chrome Timeline

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

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

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

常見性能陷阱

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() 不會拋出異常
}複製代碼

未去抖頻繁觸發的事件

某些常見的事件可能會很是頻繁的觸發,例如 scrollresize。去抖這些事件是明智的,特別是若是事件處理程序執行的不只僅是基本功能。

Lodash 有 _.debounce 方法。在 NPM 上還有一個獨立的 debounce 包.

「可是我真的須要當即反饋 scroll/resize 或者別的事件」

我發現一種能夠處理這些事件而且以高性能的方式進行響應的方法,那就是在第一次事件觸發時啓動 requestAnimationFrame() 循環。而後可使用 [debounce()](https://lodash.com/docs#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 )
  }

}複製代碼

密集CPU任務線程阻塞

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

在這些狀況下,若是有可能最好使用 Web Worker 將這些功能移到另外一個線程上,這樣咱們的主渲染線程能夠保持順滑。

相關閱讀

MDN 文章: Using Web Workers

MDN 文檔: Worker API

結語

咱們但願上述建議對您能有所幫助。若是沒有 Vixlet 團隊的偉大工做和研究,上述的提示和編程技巧是不可能產出的。他們真的是我曾經合做過的最棒的團隊之一。

在你的 React 的征途中保持學習和練習,願原力與你同在!


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索