本文主要對在React應用中可以採用的一些效能優化方式做一下總結整理

前言

目的

目前在工作中,大量的專案都是使用react來進行開展的,瞭解掌握下react的效能優化對專案的體驗和可維護性都有很大的好處,下面介紹下在react中可以運用的一些效能優化方式;

效能優化思路

對於類式元件和函式式元件來看,都可以從以下幾個方面去思考如何能夠進行效能優化

  • 減少重新render的次數
  • 減少渲染的節點
  • 降低渲染計算量
  • 合理設計元件

減少重新render的次數

在react裡時間耗時最多的一個地方是reconciliation(reconciliation 的最終目標是以最有效的方式,根據新的狀態來更新 UI,我們可以簡單地理解為 diff),如果不執行render,也就不需要reconciliation,所以可以看出減少render在效能優化過程中的重要程度了。

PureComponent

React.PureComponent 與 React.Component 很相似。兩者的區別在於 React.Component 並未實現 shouldComponentUpdate(),而 React.PureComponent 中以淺層對比 prop 和 state 的方式來實現了該函式。
需要注意的是在使用PureComponent的元件中,在props或者state的屬性值是物件的情況下,並不能阻止不必要的渲染,是因為自動載入的shouldComponentUpdate裡面做的只是淺比較,所以想要用PureComponent的特性,應該遵守原則:

  • 確保資料型別是值型別
  • 如果是引用型別,不應當有深層次的資料變化(解構)

ShouldComponentUpdate

可以利用此事件來決定何時需要重新渲染元件。如果元件 props 更改或呼叫 setState,則此函式返回一個 Boolean 值,為true則會重新渲染元件,反之則不會重新渲染元件。
在這兩種情況下元件都會重新渲染。我們可以在這個生命週期事件中放置一個自定義邏輯,以決定是否呼叫元件的 render 函式。
下面舉一個小的例子來輔助理解下:
比如要在你的應用中展示學生的詳細資料,每個學生都包含有多個屬性,如姓名、年齡、愛好、身高、體重、家庭住址、父母姓名等;在這個元件場景中,只需要展示學生的姓名、年齡、住址,其他的資訊不需要在這裡展示,所以在理想情況下,除去姓名、年齡、住址以外的資訊變化元件是不需要重新渲染的;
示例程式碼如下:

  1. import React from "react";
  2. export default class ShouldComponentUpdateUsage extends React.Component {
  3. constructor(props) {
  4. super(props);
  5. this.state = {
  6. name: "小明",
  7. age: 12,
  8. address: "xxxxxx",
  9. height: 165,
  10. weight: 40
  11. }
  12. }
  13. componentDidMount() {
  14. setTimeout(() => {
  15. this.setState({
  16. height: 168,
  17. weight: 45
  18. });
  19. }, 5000)
  20. }
  21. shouldComponentUpdate(nextProps, nextState) {
  22. if(nextState.name !== this.state.name || nextState.age !== this.state.age || nextState.address !== this.state.address) {
  23. return true;
  24. }
  25. return false;
  26. }
  27. render() {
  28. const { name, age, address } = this.state;
  29. return (
  30. <div>
  31. <p>Student name: {name} </p>
  32. <p>Student age:{age} </p>
  33. <p>Student address:{address} </p>
  34. </div>
  35. )
  36. }
  37. }

按照 React 團隊的說法,shouldComponentUpdate是保證效能的緊急出口,既然是緊急出口,那就意味著我們輕易用不到它。但既然有這樣一個緊急出口,那說明有時候它還是很有必要的。所以我們要搞清楚到底什麼時候才需要使用這個緊急出口。

使用原則

當你覺得,被改變的state或者props,不需要更新檢視時,你就應該思考要不要使用它。
需要注意的一個地方是:改變之後,又不需要更新檢視的狀態,也不應該放在state中。
shouldComponentUpdate的使用,也是有代價的。如果處理得不好,甚至比多render一次更消耗效能,另外也會使元件的複雜度增大,一般情況下使用PureComponent即可;

React.memo

如果你的元件在相同 props 的情況下渲染相同的結果,那麼你可以通過將其包裝在 React.memo 中呼叫,以此通過記憶元件渲染結果的方式來提高元件的效能表現。這意味著在這種情況下,React 將跳過渲染元件的操作並直接複用最近一次渲染的結果。
React.memo 僅檢查 props 變更。如果函式元件被 React.memo 包裹,且其實現中擁有 useState,useReducer 或 useContext 的 Hook,當 state 或 context 發生變化時,它仍會重新渲染。
預設情況下其只會對複雜物件做淺層對比,如果你想要控制對比過程,那麼請將自定義的比較函式通過第二個引數傳入來實現。

  1. function MyComponent(props) {
  2. /* 使用 props 渲染 */
  3. }
  4. function areEqual(prevProps, nextProps) {
  5. /*
  6. 如果把 nextProps 傳入 render 方法的返回結果與
  7. 將 prevProps 傳入 render 方法的返回結果一致則返回 true,
  8. 否則返回 false
  9. */
  10. }
  11. export default React.memo(MyComponent, areEqual);

注意

與 class 元件中 shouldComponentUpdate() 方法不同的是,如果 props 相等,areEqual 會返回 true;如果 props 不相等,則返回 false。這與 shouldComponentUpdate 方法的返回值相反。

合理使用Context

Context 提供了一個無需為每層元件手動新增 props,就能在元件樹間進行資料傳遞的方法。正是因為其這個特點,它是可以穿透React.memo或者shouldComponentUpdate的比對的,也就是說,一旦 Context 的 Value 變動,所有依賴該 Context 的元件會全部 forceUpdate.這個和 Mobx 和 Vue 的響應式系統不同,Context API 並不能細粒度地檢測哪些元件依賴哪些狀態。

原則

  • Context中只定義被大多陣列件所共用的屬性,例如當前使用者的資訊、主題或者選擇的語言。

避免使用匿名函式

首先來看下下面這段程式碼

  1. const MenuContainer = ({ list }) => (
  2. <Menu>
  3. {list.map((i) => (
  4. <MenuItem key={i.id} onClick={() => handleClick(i.id)} value={i.value} />
  5. ))}
  6. </Menu>
  7. );

上面這個寫法看起來是比較簡潔,但是有一個潛在問題是匿名函式在每次渲染時都會有不同的引用,這樣就會導致Menu元件會出現重複渲染的問題;可以使用useCallback來進行優化:

  1. const MenuContainer = ({ list }) => {
  2. const handleClick = useCallback(
  3. (id) => () => {
  4. // ...
  5. },
  6. [],
  7. );
  8. return (
  9. <Menu>
  10. {list.map((i) => (
  11. <MenuItem key={i.id} id={i.id} onClick={handleClick(i.id)} value={i.value} />
  12. ))}
  13. </Menu>
  14. );
  15. };

減少渲染的節點

元件懶載入

元件懶載入可以讓react應用在真正需要展示這個元件的時候再去展示,可以比較有效的減少渲染的節點數提高頁面的載入速度

React官方在16.6版本後引入了新的特性:React.lazy 和 React.Suspense,這兩個元件的配合使用可以比較方便進行元件懶載入的實現;
React.lazy
該方法主要的作用就是可以定義一個動態載入的元件,這可以直接縮減打包後bundle的體積,並且可以延遲載入在初次渲染時不需要渲染的元件,程式碼示例如下:
使用之前

  1. import SomeComponent from './SomeComponent';

使用之後

  1. const SomeComponent = React.lazy(() => import('./SomeComponent'));

使用 React.lazy 的動態引入特性需要 JS 環境支援 Promise。在 IE11 及以下版本的瀏覽器中需要通過引入 polyfill 來使用該特性。

React.Suspense
該元件目前主要的作用就是配合渲染lazy元件,這樣就可以在等待載入lazy元件時展示loading元素,不至於直接空白,提升使用者體驗;
Suspense元件中的 fallback 屬性接受任何在元件載入過程中你想展示的 React 元素。你可以將 Suspense 元件置於懶載入元件之上的任何位置,你甚至可以用一個 Suspense 元件包裹多個懶載入元件。
程式碼示例如下:

  1. import React, { Suspense } from 'react';
  2. const OtherComponent = React.lazy(() => import('./OtherComponent'));
  3. const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
  4. function MyComponent() {
  5. return (
  6. <div>
  7. <Suspense fallback={<div>Loading...</div>}>
  8. <section>
  9. <OtherComponent />
  10. <AnotherComponent />
  11. </section>
  12. </Suspense>
  13. </div>
  14. );
  15. }

有一點要特別注意的是:React.lazy 和 Suspense 技術還不支援服務端渲染。如果你想要在使用服務端渲染的應用中使用,推薦使用 Loadable Components 這個庫,可以結合這個文件服務端渲染打包指南來進行檢視。

另外在業內也有一些比較成熟的react元件懶載入開源庫:react-loadablereact-lazyload,感興趣的可以結合看下;

虛擬列表

虛擬列表是一種根據滾動容器元素的可視區域來渲染長列表資料中某一個部分資料的技術,在開發一些專案中,會遇到一些不是直接分頁來載入列表資料的場景,在這種情況下可以考慮結合虛擬列表來進行優化,可以達到根據容器元素的高度以及列表項元素的高度來顯示長列表資料中的某一個部分,而不是去完整地渲染長列表,以提高無限滾動的效能。
可以關注下放兩個比較常用的類庫來進行深入瞭解

降低渲染計算量

useMemo

先來看下useMemo的基本使用方法:

  1. function computeExpensiveValue(a, b) {
  2. // 計算量很大的一些邏輯
  3. return xxx
  4. }
  5. const memoizedValue = useMemo(computeExpensiveValue, [a, b]);

useMemo 的第一個引數就是一個函式,這個函式返回的值會被快取起來,同時這個值會作為 useMemo 的返回值,第二個引數是一個數組依賴,如果數組裡面的值有變化,那麼就會重新去執行第一個引數裡面的函式,並將函式返回的值快取起來並作為 useMemo 的返回值 。

注意

  • 如果沒有提供依賴項陣列,useMemo 在每次渲染時都會計算新的值;
  • 計算量如果很小的計算函式,也可以選擇不使用 useMemo,因為這點優化並不會作為效能瓶頸的要點,反而可能使用錯誤還會引起一些效能問題。

遍歷展示檢視時使用key

key 幫助 React 識別哪些元素改變了,比如被新增或刪除。因此你應當給陣列中的每一個元素賦予一個確定的標識。

  1. const numbers = [1, 2, 3, 4, 5];
  2. const listItems = numbers.map((number) =>
  3. <li key={number.toString()}>
  4. {number}
  5. </li>
  6. );

使用key注意事項:

  • 最好是這個元素在列表中擁有的一個獨一無二的字串。通常,我們使用資料中的 id 來作為元素的 key,當元素沒有確定 id 的時候,萬不得已你可以使用元素索引 index 作為 key
  • 元素的 key 只有放在就近的陣列上下文中才有意義。例如,如果你提取出一個 ListItem 元件,你應該把 key 保留在陣列中的這個 元素上,而不是放在 ListItem 元件中的
  • 元素上。

合理設計元件

簡化props

如果一個元件的props比較複雜的話,會影響shallowCompare的效率,也會使這個元件變得難以維護,另外也與“單一職責”的原則不符合,可以考慮進行拆解。

簡化State

在設計元件的state時,可以按照這個原則來:需要元件響應它的變動或者需要渲染到檢視中的資料,才放到 state 中;這樣可以避免不必要的資料變動導致元件重新渲染。

減少元件巢狀

一般不必要的節點巢狀都是濫用高階元件/RenderProps 導致的。所以還是那句話‘只有在必要時才使用 xxx’。 有很多種方式來代替高階元件/RenderProps,例如優先使用 props、React Hooks

參考

https://react.docschina.org/docs/optimizing-performance.html

https://www.infoq.cn/article/2016/07/react-shouldcomponentupdate