React性能優化小貼士

日常在完成業務功能開發後,不知道你是否還會精益求精,作一些性能優化方面的工做呢?React框架中有一些性能優化相關的注意事項,若是日常不怎麼關注的話,可能就很容易忽略掉。接下來的這篇文章,將圍繞工做中會用到的幾種性能優化的相關經驗進行介紹。javascript

Key

在渲染列表結構數據的時候,使用key能夠說已經成爲React開發中的最佳實踐了。那麼你知道爲何咱們要使用key嗎?緣由是使用key可以讓組件保持結構的穩定性。咱們都知道React以其DOM Diff算法而著名,在實際比對節點更新的過程當中帶有惟一性的key可以讓React更快得定位到變動的節點,從而能夠作到最小化更新。html

在實際使用過程當中,不少人經常圖方便會直接使用數組的下標(index)做爲key,這是很危險的。由於常常會對數組數據進行增刪,容易致使下標值不穩定。因此在開發過程當中,應該儘可能避免這種狀況發生。前端

下面以商品列表組件爲例,演示一下key的使用:java

class ShopMenu extends React.Component {
    render() {
        return (
            <ul> { this.props.shopItems.map((shopItem) => <ShopItem key={shopItem.id} itemName={shopItem.name}></ShopItem>) } </ul>
        )
    }
}
複製代碼

數據比對

做爲一款優秀的前端框架,React自己已經爲咱們作了不少工做。不過在開發過程當中,若是咱們能讓組件避免在非必要的狀況下從新渲染,就能使開發出的組件性能更良好。react

淺比較 shadowEqual

組件在更新過程當中,數據比對這一過程是必不可少的,它是觸發組件從新渲染的關鍵。所以,咱們有必要深刻理解React組件在更新過程當中的數據變化機制。React對於狀態更新的比較方式默認都是採用淺比較,咱們能夠看一下它的源碼實現git

/** * Performs equality by iterating through keys on an object and returning false * when any key has values which are not strictly equal between the arguments. * Returns true when the values of all keys are strictly equal. */
function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (is(objA, objB)) {
    return true;
  }

  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    if (
      !hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

  return true;
}
複製代碼

另外,對於對象作相等比對的is方法,不一樣於直接使用=====,它針對特殊的+0-0,NaNNaN的比對作了修復,而且不會作隱式轉換。它的實現是像這樣的:github

/** * inlined Object.is polyfill to avoid requiring consumers ship their own * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is */
function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y)
  );
}
複製代碼

能夠從上面的代碼中看到,對於引用對象來講,淺比較算法首先會使用Object.keys獲取對象全部的屬性,並比對對應的屬性值。不過這裏只會比對第一層的數據,並無作遞歸對比。這大概就是叫作"淺比較"的緣由吧。web

shouldComponentUpdate

對於Class組件來講,咱們可使用shouldComponentUpdate方法來判斷是否進行組件渲染,從而更好地提升頁面性能。這個方式會在每次props和state變化的時候執行,框架對於這個方法的默認實現是直接返回true,即每次只要屬性和狀態變動,組件都會從新渲染。而若是咱們對於數據的變動邏輯比較清楚,徹底能夠手動實現比對過程來避免重複渲染:算法

class ShopItem extends React.Component {
    shouldComponentUpdate(nextProps, nextState) {
        return this.props.itemName !== nextProps.itemName;
    }
    
    render() {
      return (<div>{this.props.itemName}</div>);
    }
}
複製代碼

pureComponent

要達到性能優化的目的,有時候也沒必要手動實現shouldComponentUpdate。你只要讓你的組件繼承自React.PureComponent便可,它已經內置了淺比較算法,因此上面的例子能夠改寫成:chrome

class ShopItem extends React.PureComponent {
    render() {
        return (<div>{this.props.itemName}</div>);
    }
}
複製代碼

關於箭頭函數

還有一點要記住的是,在使用箭頭函數的時候要當心:

class Button extends React.Component {
	render() {
		return <button onClick={() => {console.log('hello, scq000');}}>click</button>
	}
}
複製代碼

直接在組件上綁定箭頭函數雖然寫法簡便,但因爲每次渲染的時候都會從新生成該函數,會致使性能損耗。即便組件的其餘props或state沒有變動,因爲使用了內聯的箭頭函數也會觸發從新渲染。

因此,爲了不這種狀況的發生,咱們能夠先聲明好事件監聽函數後,而後再拿到其引用傳給組件:

class Button extends React.Component {
	handleClick = () => {
		console.log('hello, scq000');
	}
	
	render() {
		return <button onClick={this.handleClick}>click</button>
	}
}
複製代碼

useCallback

若是咱們使用的是函數式組件,React16中的useCallback的hook爲咱們提供了一種新思路:

export const Button = (text, alertMsg) => {
	const handleClick = useCallback(() => {
    	// do something with alertMsg
    }, [alertMsg]);
	return (
		<button onClick={handleClick}>{text}</button>
	);
}
複製代碼

將箭頭函數傳入useCallback方法中,這是一個高階函數,它會返回一個記憶化(memoized)的方法。這個方法只有當它所依賴的props或state變化的時候纔會更新。在上面的例子中,當它的依賴狀態alertMsg變化的時候,handleClick函數纔會更新。

在React16中,你可能還會用到useEffect這個Hook來處理一些反作用,就像這樣:

const Student = ({name, age}) => {
	useEffect(() => {
		doSomethingWithInfos(infos)
	}, [name, age]);
	
	return (
		<div>This is a child component.</div>
	);
}

const Person = () => {
	return (<Student name="scq000" age="11" />) } 複製代碼

useEffect傳入的第二個參數也是它的依賴項,若是這個依賴項中使用的是一個箭頭函數,那麼每次useEffect中的回調函數都會執行。這樣一來結果可能就不是咱們想要的了,此時也能夠藉助useCallback來避免這種狀況的發生。

useCallback雖然可以緩存函數,但對於大多數場景來講使用它反而會增長垃圾回收和運行封裝函數的時間。只有對於大計算量的函數來講,利用useCallback才能起到良好的優化效果。

useMemo

除了直接緩存函數,有時候還須要緩存數據和計算結果。實現記憶化的關鍵是記住上一次的狀態值和輸出值。咱們利用閉包就能實現一個簡化的Memorize方法:

function memorize(func) {
  let lastInput = null;
  let lastOuput = null;
  return function() {
  	// 這裏使用淺比較來判斷參數是否一致
    if (!shallowEqual(lastInput, arguments)) {
      lastOuput = func.apply(null, arguments);
    }
    lastInput = arguments;
    return lastOuput;
  }
}
複製代碼

在React中,useMemo hook已經爲咱們實現了這個功能,直接使用就能夠了:

const calcResult = React.useMemo(() => expensiveCalulate(a, b), [a, b]);
複製代碼

當輸入參數a,b沒有發生變化的時候,會自動使用上一次的值。這也意味着咱們使用useMemo只能用來緩存純函數的計算結果。對於大計算量的操做來講,能夠有效避免重複計算過程。

React.Memo

針對Functional組件來講,因爲缺乏shouldComponentUpdate方法,能夠考慮用React.Memo來優化組件性能:React.Memo是一個高階組件,它內置了useMemo方法來緩存整個組件。

考慮下面這段代碼:

function Demo() {
	return (
		<Parent props={props}>
			<Child title={title} subtitle={subtitle} />
		</Parent>
	);
}
複製代碼

父組件因爲props中的屬性變動從新渲染,即便子組件props沒有變化,子組件Child也會跟着從新渲染。這時候,能夠考慮使用React.Memo來緩存子組件:

export function Card({title, subtitle}) {
	// do some render logic
}
export const MemoziedCard = React.Memo(Card);
複製代碼

爲了更深刻地理解這部分邏輯,讓咱們看一下相關的源碼:

if (updateExpirationTime < renderExpirationTime) {
    // This will be the props with resolved defaultProps,
    // unlike current.memoizedProps which will be the unresolved ones.
    const prevProps = currentChild.memoizedProps;
    // Default to shallow comparison
    let compare = Component.compare;
    compare = compare !== null ? compare : shallowEqual;
    if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderExpirationTime,
      );
    }
  }
複製代碼

咱們能夠看到React.Memo默認狀況下也是使用的淺比較算法,因此對於複雜的數據,咱們須要本身實現數據比對邏輯。能夠在React.Memo傳入第二個參數,就像下面這樣:

const compartor = (prevProps, nextProps) => {
	return prevProps.id === nextProps.id;
}

React.Memo(Card, compartor)
複製代碼

不可變數據Immutable

Immutable是Facebook封裝好的抽象數據結構,因爲其結構的不變性和共享性,能讓引用對象在比對的時候更加快速。使用Immutable建立的數據不可變動,所以數據在整個應用中都易於追蹤。這也符合函數式編程的思想。它的核心是採用持久化的數據結構,當改變數據的時候,只會更新變動的那一部分,而數據結構中的不變部分都會公用同一引用,達到結構共享的目的。因此,在高度嵌套的數據進行深拷貝的時候,性能也會更優。

438px-Purely_functional_tree_after.png

import Immutable from 'immutable';

var obj = Immutable.fromJS({1: "one"});
var map = Immutable.Map({a: 1, b: 2, c: 3});
map.set('b', 4);
var list = Immutable.List.of(1,2,3);
list.push(5);
複製代碼

雖然Immutable JS在性能上有它的優點,但請注意使用的影響面。不要讓原生對象和Immutable對象進行混用,這樣反而會致使性能降低,由於將Immutable數據再轉換成原生JS對象在性能上是不好的。關於使用Immutable JS的最佳實踐,能夠參考這篇文章

reselect

在使用Redux過程當中,組件的狀態數據一般是從state派生出來的,要作不少計算的邏輯。 假設如今我應用中的狀態樹是這樣的:

const state = {
  a: {
    b: {
      c: 'c',
      d: 'd'
    }
  }
};
複製代碼

每次a.b.c更新的時候,即便d沒有更新,全部引用到a.b.d的地方也會從新計算。

那麼,咱們在這一步要優化的點,一樣也是使用緩存或記憶化。reselect就是爲了這個目的而生的,它能夠幫助咱們避免重複的計算:

import {createSelector} from "reselect";

const shopItemSelector = (state) => state.shopItems;
const parentSelector = (state) => state.parent;

export const shopMenuSelector = createSelector(
	[shopItemSelector, parentSelector],
	(shopItems, parent) => {
      // do something with shopItems and parent
	}
);
複製代碼

只有狀態shopItemsparent變化後,纔會從新計算。

默認狀況下,新舊屬性的比對也是採用淺比較來進行的。結合上一小節介紹的Immutable,咱們能夠進一步優化比對過程。

首先是將咱們的整個state樹改用Immutable數據結構:

const state = Immutable.fromJS(originState);
複製代碼

接着,改寫派生狀態的時候,使用Immutable中的is進行比對:

import {createSelectorCreator, defaultMemoize} from 'reselect';
import { is } from 'immutable';

const createImmutableSelector = createSelector(defaultMemoize, is);

export const shopMenuSelector = createImmutableSelector(
	[shopItemSelector, parentSelector],
	(shopItems, parent) => {
      // do something with shopItems and parent
	}
);
複製代碼

按需加載

上面介紹的優化方式主要都是圍繞組件渲染機制來展開的,而接下來要介紹的方法是依靠延遲計算思想來優化應用響應性能。雖然並不能達到減小總渲染時間的目的,但能夠更快地讓用戶跟頁面進行交互,從而提升應用的用戶體驗。

在React 16以前,咱們通常要實現懶加載可使用react-loadable等庫,但如今能夠直接使用React.lazy方法就能夠了。本質上它也是經過代碼拆分的方式,讓部分非核心的組件延遲加載。要使用React.lazy還須要配合Suspense組件一塊兒。Suspense組件能夠爲懶加載組件提供基本的過渡效果,一般狀況下是提供一個loading動畫:

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

function MyComponent() {
  return (
    <div> <Suspense fallback={<div>Loading...</div>}> <OtherComponent /> </Suspense> </div>
  );
}
複製代碼

不過這一策略,目前只支持瀏覽器端。至於使用了SSR的React應用,能夠考慮https://github.com/smooth-code/loadable-components來達到相同的目的。

測試性能

著名管理學大師彼得.德魯克(Peter Drucker)曾說過"If you can't measure it, you can't improve it."。雖然這句話是說管理學中的事情,但放在軟件開發中也是一樣適用的。在考慮優化React頁面性能以前,咱們必需要作好對應的測試工做,找到性能瓶頸。使用React DevTools Profiler能夠檢測組件渲染性能,這個工具能夠在谷歌商店下載到。 [圖片上傳失敗...(image-e0f95d-1564019981226)] 更具體的使用方式能夠參考reactjs.org/blog/2018/0…

總結

性能優化永遠是軟件開發中的痛點和難點,要學習和實踐的知識有不少,只能說任重而道遠。不過在工做中也並不提倡過早優化。性能雖然是重要的評判標準,但在開發過程當中還必須在代碼的可維護性、對將來的適應性等方面作出取捨。應用中並不是全部的部分都必須快如閃電,有些部分的可維護性每每更加劇要。

若是必定要作性能優化,核心仍是在減小頻繁計算和渲染上,在實現策略上主要有三種方式:利用key維持組件結構穩定性、優化數據比對過程和按需加載。其中優化數據比對過程能夠根據具體使用的場景,分別使用緩存數據或組件、改用Immutable不可變數據等方式進行。最後,也必定記得要採用測試工具進行先後性能對比,來保障優化工做的有效性。

參考文章

www.ayqy.net/blog/react-…

zhuanlan.zhihu.com/p/56975681

codeburst.io/memorized-f…

blog.bitsrc.io/lazy-loadin…

kentcdodds.com/blog/usemem…

reactjs.org/docs/optimi…

reactjs.org/blog/2018/0…

redux.js.org/recipes/usi…

——轉載請註明出處———

微信掃描二維碼,關注個人公衆號
最後,歡迎你們關注個人公衆號,一塊兒學習交流。
相關文章
相關標籤/搜索