本文主要對在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 函數。
下面舉一個小的例子來輔助理解下:
比如要在你的應用中展示學生的詳細資料,每個學生都包含有多個屬性,如姓名、年齡、愛好、身高、體重、家庭住址、父母姓名等;在這個組件場景中,只需要展示學生的姓名、年齡、住址,其他的信息不需要在這里展示,所以在理想情況下,除去姓名、年齡、住址以外的信息變化組件是不需要重新渲染的;
示例代碼如下:
import React from "react";
export default class ShouldComponentUpdateUsage extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "小明",
age: 12,
address: "xxxxxx",
height: 165,
weight: 40
}
}
componentDidMount() {
setTimeout(() => {
this.setState({
height: 168,
weight: 45
});
}, 5000)
}
shouldComponentUpdate(nextProps, nextState) {
if(nextState.name !== this.state.name || nextState.age !== this.state.age || nextState.address !== this.state.address) {
return true;
}
return false;
}
render() {
const { name, age, address } = this.state;
return (
<div>
<p>Student name: {name} </p>
<p>Student age:{age} </p>
<p>Student address:{address} </p>
</div>
)
}
}
按照 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 發生變化時,它仍會重新渲染。
默認情況下其只會對復雜對象做淺層對比,如果你想要控制對比過程,那么請將自定義的比較函數通過第二個參數傳入來實現。
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 傳入 render 方法的返回結果與
將 prevProps 傳入 render 方法的返回結果一致則返回 true,
否則返回 false
*/
}
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中只定義被大多數組件所共用的屬性,例如當前用戶的信息、主題或者選擇的語言。
避免使用匿名函數
首先來看下下面這段代碼
const MenuContainer = ({ list }) => (
<Menu>
{list.map((i) => (
<MenuItem key={i.id} onClick={() => handleClick(i.id)} value={i.value} />
))}
</Menu>
);
上面這個寫法看起來是比較簡潔,但是有一個潛在問題是匿名函數在每次渲染時都會有不同的引用,這樣就會導致Menu組件會出現重復渲染的問題;可以使用useCallback來進行優化:
const MenuContainer = ({ list }) => {
const handleClick = useCallback(
(id) => () => {
// ...
},
[],
);
return (
<Menu>
{list.map((i) => (
<MenuItem key={i.id} id={i.id} onClick={handleClick(i.id)} value={i.value} />
))}
</Menu>
);
};
減少渲染的節點
組件懶加載
組件懶加載可以讓react應用在真正需要展示這個組件的時候再去展示,可以比較有效的減少渲染的節點數提高頁面的加載速度
React官方在16.6版本后引入了新的特性:React.lazy 和 React.Suspense,這兩個組件的配合使用可以比較方便進行組件懶加載的實現;
React.lazy
該方法主要的作用就是可以定義一個動態加載的組件,這可以直接縮減打包后bundle的體積,並且可以延遲加載在初次渲染時不需要渲染的組件,代碼示例如下:
使用之前
import SomeComponent from './SomeComponent';
使用之后
const SomeComponent = React.lazy(() => import('./SomeComponent'));
使用 React.lazy 的動態引入特性需要 JS 環境支持 Promise。在 IE11 及以下版本的瀏覽器中需要通過引入 polyfill 來使用該特性。
React.Suspense
該組件目前主要的作用就是配合渲染lazy組件,這樣就可以在等待加載lazy組件時展示loading元素,不至於直接空白,提升用戶體驗;
Suspense組件中的 fallback 屬性接受任何在組件加載過程中你想展示的 React 元素。你可以將 Suspense 組件置於懶加載組件之上的任何位置,你甚至可以用一個 Suspense 組件包裹多個懶加載組件。
代碼示例如下:
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</div>
);
}
有一點要特別注意的是:React.lazy 和 Suspense 技術還不支持服務端渲染。如果你想要在使用服務端渲染的應用中使用,推薦使用 Loadable Components 這個庫,可以結合這個文檔服務端渲染打包指南來進行查看。
另外在業內也有一些比較成熟的react組件懶加載開源庫:react-loadable和react-lazyload,感興趣的可以結合看下;
虛擬列表
虛擬列表是一種根據滾動容器元素的可視區域來渲染長列表數據中某一個部分數據的技術,在開發一些項目中,會遇到一些不是直接分頁來加載列表數據的場景,在這種情況下可以考慮結合虛擬列表來進行優化,可以達到根據容器元素的高度以及列表項元素的高度來顯示長列表數據中的某一個部分,而不是去完整地渲染長列表,以提高無限滾動的性能。
可以關注下放兩個比較常用的類庫來進行深入了解
降低渲染計算量
useMemo
先來看下useMemo的基本使用方法:
function computeExpensiveValue(a, b) {
// 計算量很大的一些邏輯
return xxx
}
const memoizedValue = useMemo(computeExpensiveValue, [a, b]);
useMemo 的第一個參數就是一個函數,這個函數返回的值會被緩存起來,同時這個值會作為 useMemo 的返回值,第二個參數是一個數組依賴,如果數組里面的值有變化,那么就會重新去執行第一個參數里面的函數,並將函數返回的值緩存起來並作為 useMemo 的返回值 。
注意
- 如果沒有提供依賴項數組,useMemo 在每次渲染時都會計算新的值;
- 計算量如果很小的計算函數,也可以選擇不使用 useMemo,因為這點優化並不會作為性能瓶頸的要點,反而可能使用錯誤還會引起一些性能問題。
遍歷展示視圖時使用key
key 幫助 React 識別哪些元素改變了,比如被添加或刪除。因此你應當給數組中的每一個元素賦予一個確定的標識。
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
使用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