React性能優(yōu)化總結(jié)
前言
目的
目前在工作中,大量的項(xiàng)目都是使用react來進(jìn)行開展的,了解掌握下react的性能優(yōu)化對(duì)項(xiàng)目的體驗(yàn)和可維護(hù)性都有很大的好處,下面介紹下在react中可以運(yùn)用的一些性能優(yōu)化方式;
性能優(yōu)化思路
對(duì)于類式組件和函數(shù)式組件來看,都可以從以下幾個(gè)方面去思考如何能夠進(jìn)行性能優(yōu)化
- 減少重新render的次數(shù)
- 減少渲染的節(jié)點(diǎn)
- 降低渲染計(jì)算量
- 合理設(shè)計(jì)組件
減少重新render的次數(shù)
在react里時(shí)間耗時(shí)最多的一個(gè)地方是reconciliation(reconciliation 的最終目標(biāo)是以最有效的方式,根據(jù)新的狀態(tài)來更新 UI,我們可以簡(jiǎn)單地理解為 diff),如果不執(zhí)行render,也就不需要reconciliation,所以可以看出減少render在性能優(yōu)化過程中的重要程度了。
PureComponent
React.PureComponent 與 React.Component 很相似。兩者的區(qū)別在于 React.Component 并未實(shí)現(xiàn) shouldComponentUpdate(),而 React.PureComponent 中以淺層對(duì)比 prop 和 state 的方式來實(shí)現(xiàn)了該函數(shù)。
需要注意的是在使用PureComponent的組件中,在props或者state的屬性值是對(duì)象的情況下,并不能阻止不必要的渲染,是因?yàn)樽詣?dòng)加載的shouldComponentUpdate里面做的只是淺比較,所以想要用PureComponent的特性,應(yīng)該遵守原則:
- 確保數(shù)據(jù)類型是值類型
- 如果是引用類型,不應(yīng)當(dāng)有深層次的數(shù)據(jù)變化(解構(gòu))
ShouldComponentUpdate
可以利用此事件來決定何時(shí)需要重新渲染組件。如果組件 props 更改或調(diào)用 setState,則此函數(shù)返回一個(gè) Boolean 值,為true則會(huì)重新渲染組件,反之則不會(huì)重新渲染組件。
在這兩種情況下組件都會(huì)重新渲染。我們可以在這個(gè)生命周期事件中放置一個(gè)自定義邏輯,以決定是否調(diào)用組件的 render 函數(shù)。
下面舉一個(gè)小的例子來輔助理解下:
比如要在你的應(yīng)用中展示學(xué)生的詳細(xì)資料,每個(gè)學(xué)生都包含有多個(gè)屬性,如姓名、年齡、愛好、身高、體重、家庭住址、父母姓名等;在這個(gè)組件場(chǎng)景中,只需要展示學(xué)生的姓名、年齡、住址,其他的信息不需要在這里展示,所以在理想情況下,除去姓名、年齡、住址以外的信息變化組件是不需要重新渲染的;
示例代碼如下:
- 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 團(tuán)隊(duì)的說法,shouldComponentUpdate是保證性能的緊急出口,既然是緊急出口,那就意味著我們輕易用不到它。但既然有這樣一個(gè)緊急出口,那說明有時(shí)候它還是很有必要的。所以我們要搞清楚到底什么時(shí)候才需要使用這個(gè)緊急出口。
使用原則
當(dāng)你覺得,被改變的state或者props,不需要更新視圖時(shí),你就應(yīng)該思考要不要使用它。
需要注意的一個(gè)地方是:改變之后,又不需要更新視圖的狀態(tài),也不應(yīng)該放在state中。
shouldComponentUpdate的使用,也是有代價(jià)的。如果處理得不好,甚至比多render一次更消耗性能,另外也會(huì)使組件的復(fù)雜度增大,一般情況下使用PureComponent即可;
React.memo
如果你的組件在相同 props 的情況下渲染相同的結(jié)果,那么你可以通過將其包裝在 React.memo 中調(diào)用,以此通過記憶組件渲染結(jié)果的方式來提高組件的性能表現(xiàn)。這意味著在這種情況下,React 將跳過渲染組件的操作并直接復(fù)用最近一次渲染的結(jié)果。
React.memo 僅檢查 props 變更 。如果函數(shù)組件被 React.memo 包裹,且其實(shí)現(xiàn)中擁有 useState,useReducer 或 useContext 的 Hook,當(dāng) state 或 context 發(fā)生變化時(shí),它仍會(huì)重新渲染。
默認(rèn)情況下其只會(huì)對(duì)復(fù)雜對(duì)象做淺層對(duì)比,如果你想要控制對(duì)比過程,那么請(qǐng)將自定義的比較函數(shù)通過第二個(gè)參數(shù)傳入來實(shí)現(xiàn)。
- function MyComponent(props) {
- /* 使用 props 渲染 */
- }
- function areEqual(prevProps, nextProps) {
- /*
- 如果把 nextProps 傳入 render 方法的返回結(jié)果與
- 將 prevProps 傳入 render 方法的返回結(jié)果一致則返回 true,
- 否則返回 false
- */
- }
- export default React.memo(MyComponent, areEqual);
注意
與 class 組件中 shouldComponentUpdate() 方法不同的是,如果 props 相等,areEqual 會(huì)返回 true;如果 props 不相等,則返回 false。這與 shouldComponentUpdate 方法的返回值相反。
合理使用Context
Context 提供了一個(gè)無需為每層組件手動(dòng)添加 props,就能在組件樹間進(jìn)行數(shù)據(jù)傳遞的方法。正是因?yàn)槠溥@個(gè)特點(diǎn),它是可以穿透React.memo或者shouldComponentUpdate的比對(duì)的,也就是說,一旦 Context 的 Value 變動(dòng),所有依賴該 Context 的組件會(huì)全部 forceUpdate.這個(gè)和 Mobx 和 Vue 的響應(yīng)式系統(tǒng)不同,Context API 并不能細(xì)粒度地檢測(cè)哪些組件依賴哪些狀態(tài)。
原則
- Context中只定義被大多數(shù)組件所共用的屬性,例如當(dāng)前用戶的信息、主題或者選擇的語言。
避免使用匿名函數(shù)
首先來看下下面這段代碼
- const MenuContainer = ({ list }) => (
- <Menu>
- {list.map((i) => (
- <MenuItem key={i.id} onClick={() => handleClick(i.id)} value={i.value} />
- ))}
- </Menu>
- );
上面這個(gè)寫法看起來是比較簡(jiǎn)潔,但是有一個(gè)潛在問題是匿名函數(shù)在每次渲染時(shí)都會(huì)有不同的引用,這樣就會(huì)導(dǎo)致Menu組件會(huì)出現(xiàn)重復(fù)渲染的問題;可以使用useCallback來進(jìn)行優(yōu)化:
- 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>
- );
- };
減少渲染的節(jié)點(diǎn)
組件懶加載
組件懶加載可以讓react應(yīng)用在真正需要展示這個(gè)組件的時(shí)候再去展示,可以比較有效的減少渲染的節(jié)點(diǎn)數(shù)提高頁面的加載速度
React官方在16.6版本后引入了新的特性:React.lazy 和 React.Suspense,這兩個(gè)組件的配合使用可以比較方便進(jìn)行組件懶加載的實(shí)現(xiàn);
React.lazy
該方法主要的作用就是可以定義一個(gè)動(dòng)態(tài)加載的組件,這可以直接縮減打包后bundle的體積,并且可以延遲加載在初次渲染時(shí)不需要渲染的組件,代碼示例如下:
使用之前
- import SomeComponent from './SomeComponent';
使用之后
- const SomeComponent = React.lazy(() => import('./SomeComponent'));
使用 React.lazy 的動(dòng)態(tài)引入特性需要 JS 環(huán)境支持 Promise。在 IE11 及以下版本的瀏覽器中需要通過引入 polyfill 來使用該特性。
React.Suspense
該組件目前主要的作用就是配合渲染lazy組件,這樣就可以在等待加載lazy組件時(shí)展示loading元素,不至于直接空白,提升用戶體驗(yàn);
Suspense組件中的 fallback 屬性接受任何在組件加載過程中你想展示的 React 元素。你可以將 Suspense 組件置于懶加載組件之上的任何位置,你甚至可以用一個(gè) Suspense 組件包裹多個(gè)懶加載組件。
代碼示例如下:
- 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>
- );
- }
有一點(diǎn)要特別注意的是:React.lazy 和 Suspense 技術(shù)還不支持服務(wù)端渲染。如果你想要在使用服務(wù)端渲染的應(yīng)用中使用,推薦使用 Loadable Components 這個(gè)庫,可以結(jié)合這個(gè)文檔 服務(wù)端渲染打包指南 來進(jìn)行查看。
另外在業(yè)內(nèi)也有一些比較成熟的react組件懶加載開源庫: react-loadable 和 react-lazyload ,感興趣的可以結(jié)合看下;
虛擬列表
虛擬列表是一種根據(jù)滾動(dòng)容器元素的可視區(qū)域來渲染長列表數(shù)據(jù)中某一個(gè)部分?jǐn)?shù)據(jù)的技術(shù),在開發(fā)一些項(xiàng)目中,會(huì)遇到一些不是直接分頁來加載列表數(shù)據(jù)的場(chǎng)景,在這種情況下可以考慮結(jié)合虛擬列表來進(jìn)行優(yōu)化,可以達(dá)到根據(jù)容器元素的高度以及列表項(xiàng)元素的高度來顯示長列表數(shù)據(jù)中的某一個(gè)部分,而不是去完整地渲染長列表,以提高無限滾動(dòng)的性能。
可以關(guān)注下放兩個(gè)比較常用的類庫來進(jìn)行深入了解
- react-virtualized
- react-window
降低渲染計(jì)算量
useMemo
先來看下useMemo的基本使用方法:
- function computeExpensiveValue(a, b) {
- // 計(jì)算量很大的一些邏輯
- return xxx
- }
- const memoizedValue = useMemo(computeExpensiveValue, [a, b]);
useMemo 的第一個(gè)參數(shù)就是一個(gè)函數(shù),這個(gè)函數(shù)返回的值會(huì)被緩存起來,同時(shí)這個(gè)值會(huì)作為 useMemo 的返回值,第二個(gè)參數(shù)是一個(gè)數(shù)組依賴,如果數(shù)組里面的值有變化,那么就會(huì)重新去執(zhí)行第一個(gè)參數(shù)里面的函數(shù),并將函數(shù)返回的值緩存起來并作為 useMemo 的返回值 。
注意
- 如果沒有提供依賴項(xiàng)數(shù)組,useMemo 在每次渲染時(shí)都會(huì)計(jì)算新的值;
- 計(jì)算量如果很小的計(jì)算函數(shù),也可以選擇不使用 useMemo,因?yàn)檫@點(diǎn)優(yōu)化并不會(huì)作為性能瓶頸的要點(diǎn),反而可能使用錯(cuò)誤還會(huì)引起一些性能問題。
遍歷展示視圖時(shí)使用key
key 幫助 React 識(shí)別哪些元素改變了,比如被添加或刪除。因此你應(yīng)當(dāng)給數(shù)組中的每一個(gè)元素賦予一個(gè)確定的標(biāo)識(shí)。
- const numbers = [1, 2, 3, 4, 5];
- const listItems = numbers.map((number) =>
- <li key={number.toString()}>
- {number}
- </li>
- );
使用key注意事項(xiàng):
- 最好是這個(gè)元素在列表中擁有的一個(gè)獨(dú)一無二的字符串。通常,我們使用數(shù)據(jù)中的 id 來作為元素的 key,當(dāng)元素沒有確定 id 的時(shí)候,萬不得已你可以使用元素索引 index 作為 key
- 元素的 key 只有放在就近的數(shù)組上下文中才有意義。例如,如果你提取出一個(gè) ListItem 組件,你應(yīng)該把 key 保留在數(shù)組中的這個(gè) 元素上,而不是放在 ListItem 組件中的
- 元素上。
合理設(shè)計(jì)組件
簡(jiǎn)化props
如果一個(gè)組件的props比較復(fù)雜的話,會(huì)影響shallowCompare的效率,也會(huì)使這個(gè)組件變得難以維護(hù),另外也與“單一職責(zé)”的原則不符合,可以考慮進(jìn)行拆解。
簡(jiǎn)化State
在設(shè)計(jì)組件的state時(shí),可以按照這個(gè)原則來:需要組件響應(yīng)它的變動(dòng)或者需要渲染到視圖中的數(shù)據(jù),才放到 state 中;這樣可以避免不必要的數(shù)據(jù)變動(dòng)導(dǎo)致組件重新渲染。