banner
他山之石

他山之石

編寫高性能的React代碼

本文總結了一些編寫 React 代碼的建議,有助於理解 React 的渲染機制,主要是通過減少不必要的重新渲染來提升 React 性能。

image

回顧使用 React 開發的經歷,已有接近兩年了,一直想要寫一篇文章來介紹如何編寫高性能的 React 代碼,又或者是性能調優之類的,於是,我總結了一些編寫高性能 React 代碼的實踐,或許能讓你對 React 的理解更加深刻。

影響 React 性能的主要因素#

React 專注於構建用戶界面,內部通過 VirtualDOM 和 Diff 算法實現視圖的更新,本身擁有很高的性能。當組件的 state 和 props 改變的時候,React 就會重新 render,進而更新視圖。在 render 過程中,react 會將 JSX 語法轉換為虛擬 DOM,同時進行 Diff 算法比較新舊 DOM 的變化,從而對視圖進行局部更新。試想,當 render 執行後,JSX 轉換虛擬 DOM,然後 Diff,最後發現新舊 DOM 是一樣的,最後並沒有更新視圖。那麼,就導致了不必要的 render,造成了性能的浪費。所以,減少不必要的 render,成為優化 React 性能的主要因素。

state 只與視圖有關#

編寫 React 組件的時候,你肯定會用到變量,變量可以保存在 this 上,可以保存到 state 中,還可以使用全局變量。那麼什麼時候該使用 state 呢?答案是只有與視圖相關的變量才放在 state 中,與視圖無關的可以放在 this 上。如果與視圖更新無關的變量放在 state 中,而且這個變量需要頻繁的更新,而每次更新都需要重新 render,勢必造成性能浪費,使用 this 來存放無疑是更好的選擇。

shouldComponentUpdate#

考慮如下代碼:

export default class Home extends Component {
  state = {
    count: 0
  }

  add = () => {
    this.setState({count: 0});
  }
  
  render() {
    console.log('render');
    return (
      <div className="App">
        <div onClick={this.add}>+++</div>
        <p className="App-intro">
          {this.state.count}
        </p>
      </div>
    );
  }
}

當點擊 +++ 觸發 add 事件時,setState 更新 state,但是新舊 state 完全一樣,React 並沒有在 Component 組件中進行優化,導致不必要的 render 執行。此時,需要我們手動進行優化,當 state 或者 props 更新後 react 組件的生命週期 shouldComponentUpdate 觸發,在 shouldComponentUpdate 中可以對新舊 state 和 props 進行比較,判斷組件是否需要重新 render。

shouldComponentUpdate(nextProps, nextState) {
    if(nextState.count === this.state.count) {
      return false;
    }
}

默認情況下 shouldComponentUpdate 返回 true,即始終要重新渲染,此處我們使用 shouldComponentUpdate 進行比較後,返回 false,react 將不會重新 render。

當 state 為字符串等基本類型的時候,shouldComponentUpdate 還能應付自如,那如果 state 是對象或者數組等引用類型呢?對於引用類型只能對其進行遞歸比較才能判斷其是否相等,又或者是採用 JSON.stringify (nextState.obj) === JSON.stringify (this.state.obj) 進行比較,但是當對象嵌套層級較深或者函數、正則等類型時,shouldComponentUpdate 便失去了它的用武之地。

PureComponent#

React.PureComponent 與 React.Component 幾乎完全相同,但 React.PureComponent 通過 prop 和 state 的淺對比來實現 shouldComponentUpdate ()。
簡而言之,PureComponent 自己內部實現了一個 shouldComponentUpdate,而不需要我們再去編寫比較代碼。
React.PureComponent 的 shouldComponentUpdate () 只會對對象進行淺對比。如果對象包含複雜的數據結構,它可能會因深層的數據不一致而產生錯誤的否定判斷(表現為對象深層的數據已改變視圖卻沒有更新)。
測試表明,使用 PureComponent 的性能會略勝 shouldComponentUpdate 一籌。

StateLessComponent#

無狀態組件(StateLessComponent)又稱函數式組件。

const Text = ({ children = 'Hello World!' }) =>
  <p>{children}</p>

函數式組件沒有 class 聲明,沒有生命週期,僅使用 render 方法實現,不存在組件實例化的過程,因此不需要分配多餘的內存,從而提升了渲染性能。

箭頭函數與 bind#

當我們在組件上綁定事件處理函數的時候,為了能正常訪問 state,通常會使用 bind 或者箭頭函數。

<Button onClick={this.update.bind(this)} />

又或者

<Button onClick={() => { console.log("Click"); }} />

在以上情況下,bind 每次都返回全新的函數,箭頭函數在每次 render 時都會重新分配(和使用 bind 的方式相同),對於組件來說每次綁定的都是新的函數,Button 組件在進行 props 比較的時候會認為前後 props 不一樣,造成重新渲染。
比較推薦的寫法有兩種:

update = () => {
    //
}
<Button onClick={this.update} />

或者

constructor(props) {
    super(props);
    this.update = this.update.bind(this);
}

update() {
    //
}
<Button onClick={this.update} />

內聯 style#

當我們為組件設置樣式的時候,可能會寫出如下代碼:

<Button style={{width: 100, marginTop: 50 }} />

這種寫法,每次 render 時 style 返回的都是全新的對象,我們知道,引用類型比較的是引用地址,每次的結果都是不相等,也會導致重新渲染。
推薦的寫法是使用 className。

<Button className="btn" />

屬性傳遞與 []#

當我們為組件設置缺省值的時候:

<RadioGroup options={this.props.options || []} />

如果每次 this.props.options 值都是 null 的話,意味著每次傳遞給都是字面量數組 [],但字面量數組和 new Array () 效果是一樣的,始終生成新的實例,所以表面上看雖然每次傳遞給組件的都是相同的空數組,其實對組件來說每次都是新的屬性,都会引起渲染。所以正確的方式應該將一些常用值以變量的形式保存下來:

const DEFAULT_OPTIONS = [];

<RadioGroup options={this.props.options || DEFAULT_OPTIONS} />
注意:此處的DEFAULT_OPTIONS不能寫在render中,當寫在render中的時候,還是會每次都生成一個新的數組,依然會重新渲染,正確的做法是放在組件外面或者使用this.DEFAULT_OPTIONS = []。

map 與 key#

在 react 中,你應該經常用到 map 來渲染一個列表,當你忘記為每個列表元素設置 key 屬性的時候,react 會在控制台發出警告 a key should be provided for list items。
實際上,Keys 可以在 DOM 中的某些元素被增加或刪除的時候幫助 React 識別哪些元素發生了變化。因此你應當給數組中的每一個元素賦予一個確定的標識。

一個元素的 key 最好是這個元素在列表中擁有的一個獨一無二的字符串。通常,我們使用來自數據的 id 作為元素的 key:

const todoItems = todos.map((todo) =>
  <li key={todo.id}>
    {todo.text}
  </li>
);

當元素沒有確定的 id 時,你可以使用他的序列號索引 index 作為 key:

const todoItems = todos.map((todo, index) =>
  // Only do this if items have no stable IDs
  <li key={index}>
    {todo.text}
  </li>
);

當 todoItems 中數據發生改變的時候,使用索引作為 key 的列表都將重新渲染。所以不建議使用索引來進行排序,因為這會導致渲染變得很慢。

減少在 render 內執行變量#

我們已經知道,當組件的 state 和 props 發生改變的時候,render 都會執行,那麼,當 render 中有變量的執行或者函數執行的時候,可能會影響渲染性能,所以,應該減少在 render 內執行變量或者函數。

精簡 props 和 state#

從上面我們已經知道,當 props 和 state 為引用類型的時候,react 內部只對 props 和 state 進行淺比較,那麼,props 和 state 可以儘量設置為基礎數據類型,對象的嵌套層級不要過深,減少 props 的參數數量,使組件的 props 和 state 對象數據盡可能達到扁平化,都可以在一定程度上優化 react 的渲染性能。

總結#

本文討論了一系列編寫 React 代碼的原則,旨在從減少重新 render 的方面來提升 react 代碼的性能。性能優化一直是一個持久的話題,還可以從很多方面去考慮,比如,使用不可變的數據結構 immutable.js,使用 reselect 來緩存 redux 中的複雜計算,對組件儘可能進行拆分為子組件,都是值得探討的話題。那麼,下次再見。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。