react的一些性能優化

react 性能優化

shouldComponentUpdate

對於對視圖沒有關聯,但可能更新的數據,可以用shouldComponentUpdate過濾掉,減少無所謂的更新

返回一個Boolean值,true則更新,false則不更新,默認爲true

 shouldComponentUpdate(nextProps,nextState){
 if(this.props.xxx !== nextProps.xxx){
      return true;
  }else {
      return false;
  }
 }

PureComponent

對組件的stateprops僅淺對比更新,即傳入的是對象,單純的更改對象屬性是無法觸發更新,需要更改對象的引用(對於非基本類型的對比,對比的是引用的地址塊)
可以配合immutable.js使用,以達到對象更改屬性時更新的效果。

Redux配合immutable.js使用

Immutable Data 就是一旦創建,就不能再被更改的數據。對 Immutable 對象的任何修改或添加刪除操作都會返回一個新的 Immutable 對象。Immutable 實現的原理是 Persistent Data Structure(持久化數據結構)。

Immutable.js瞭解一下? - 掘金

memo

React.memo 爲高階組件。它與 React.PureComponent 非常相似,但它適用於函數組件,但不適用於 class 組件
如果你的函數組件在給定相同 props 的情況下渲染相同的結果,那麼你可以通過將其包裝在 React.memo 中調用,以此通過記憶組件渲染結果的方式來提高組件的性能表現。這意味着在這種情況下,React 將跳過渲染組件的操作並直接複用最近一次渲染的結果。

原理同PureComponent,但僅對props僅做淺對比。
如果要手動添加對比過程,可以在第二個參數傳入自定義的對比函數。

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /* 如果函數返回 true,就會跳過更新。*/
   if(nextProps.xxx !== prevProps.xxx){
      return false;
  }else {
      return true;
  }
}
export default React.memo(MyComponent, areEqual);

useMemo

useMemo允許你通過「記住」上一次計算結果的方式在多次渲染的之間緩存計算結果。

memo的區別是useMemo作用於計算以及組件。

  • 第一個參數是主體內容
  • 第二個參數是數組,傳入要記住的參數

作用與function:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

作用於組件:

function Parent({ a, b }) {
  // Only re-rendered if `a` changes:
  const child1 = useMemo(() => <Child1 a={a} />, [a]);
  // Only re-rendered if `b` changes:
  const child2 = useMemo(() => <Child2 b={b} />, [b]);
  return (
    <>
      {child1}
      {child2}
    </>
  )
}
注意這種方式在循環中是無效的,因爲 Hook 調用 不能 被放在循環中。但你可以爲列表項抽取一個單獨的組件,並在其中調用 useMemo

useCallback

useCallbackuseMemo 有些相似。它接收一個內聯函數和一個數組,它返回的是一個記憶化版本的函數。

useCallback(fn, deps) 相當於 useMemo(() => fn, deps)

lazy & Suspense

React 集成了lazySuspense
lazy會進行代碼分割,它能讓你像渲染常規組件一樣處理動態引入(的組件)。。
fallback 屬性接受任何在組件加載過程中你想展示的 React 元素。你可以將 Suspense 組件置於懶加載組件之上的任何位置。你甚至可以用一個 Suspense 組件包裹多個懶加載組件。

👇對路由組件進行代碼分割、懶加載。

import React, { lazy, Suspense } from "react";
import { renderRoutes } from "react-router-config";
import { HashRouter, Redirect } from "react-router-dom";

const Page1Comp = lazy(() => import("../Page1"));
const Page1 = props => {
  return (
    <Suspense fallback={<p>loading</p>}>
      <Page1Comp {...props} />
    </Suspense>
  );
};

const Page2Comp = lazy(() => import("../Page2"));
const Page2 = props => {
  return (
    <Suspense fallback={<p>loading</p>}>
      <Page2Comp {...props} />
    </Suspense>
  );
};

const routes = [
  { path: "/", exact: true, render: () => <Redirect to={"/page1"} /> },
  { path: "/page1", component: Page1 },
  { path: "/page2", component: Page2 }
];

function App() {
  return (
    <HashRouter>
      <div className="App">
        <h1>Hello</h1>
        {renderRoutes(routes)}
      </div>
    </HashRouter>
  );
}

export default App;

Fragment

在jsx裏面,會有很多無意義但必須有的的嵌套。可以使用Fragment代替減少無謂的嵌套。
render() {
  return (
    <React.Fragment>
      <ChildA />
      <ChildB />
      <ChildC />
    </React.Fragment>
  );
}
// or
render() {
  return (
    <>
      <ChildA />
      <ChildB />
      <ChildC />
    </>
  );
}

不適用內聯函數

<input type="button" onClick={(e) => { this.setState({inputValue: e.target.value}) }} />

👆上面的函數創建了內聯函數。每次調用 render 函數時都會創建一個函數的新實例,render 函數會將該函數的新實例綁定到該按鈕。
改成如下👇:

export default class InlineFunctionComponent extends Component {
  setNewStateData = (event) => {
    this.setState({
      inputValue: e.target.value
    })
  }

  render() {
    return <input type="button" onClick={this.setNewStateData} />
  }
}

函數綁定在constructor完成

<input type="button" value="Click" onClick={this.handleButtonClick.bind(this)} />

每次調用 render 函數時都會創建並使用綁定到當前上下文的新函數,但在每次渲染時使用已存在的函數效率更高。優化方案如下:

constructor() {
  this.handleButtonClick = this.handleButtonClick.bind(this)
}

箭頭函數與constructor函數綁定

當我們添加箭頭函數時,該函數被添加爲對象實例,而不是類的原型屬性。這意味着如果我們多次複用組件,那麼在組件外創建的每個對象中都會有這些函數的多個實例。
每個組件都會有這些函數的一份實例,影響了可複用性。此外因爲它是對象屬性而不是原型屬性,所以這些函數在繼承鏈中不可用。
因此箭頭函數確實有其缺點。實現這些函數的最佳方法是在構造函數中綁定函數。

節流和防抖

節流函數

節流是將多次執行變成每隔一段時間執行。

/**
 * underscore 節流函數,返回函數連續調用時,func 執行頻率限定爲 次 / wait
 *
 * @param  {function}   func      回調函數
 * @param  {number}     wait      表示時間窗口的間隔
 * @param  {object}     options   如果想忽略開始函數的的調用,傳入{leading: false}。
 *                                如果想忽略結尾函數的調用,傳入{trailing: false}
 *                                兩者不能共存,否則函數不能執行
 * @return {function}             返回客戶調用函數
 */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 之前的時間戳
    var previous = 0;
    // 如果 options 沒傳則設爲空對象
    if (!options) options = {};
    // 定時器回調函數
    var later = function() {
      // 如果設置了 leading,就將 previous 設爲 0
      // 用於下面函數的第一個 if 判斷
      previous = options.leading === false ? 0 : _.now();
      // 置空一是爲了防止內存泄漏,二是爲了下面的定時器判斷
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      // 獲得當前時間戳
      var now = _.now();
      // 首次進入前者肯定爲 true
      // 如果需要第一次不執行函數
      // 就將上次時間戳設爲當前的
      // 這樣在接下來計算 remaining 的值時會大於0
      if (!previous && options.leading === false) previous = now;
      // 計算剩餘時間
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 如果當前調用已經大於上次調用時間 + wait
      // 或者用戶手動調了時間
       // 如果設置了 trailing,只會進入這個條件
      // 如果沒有設置 leading,那麼第一次會進入這個條件
      // 還有一點,你可能會覺得開啓了定時器那麼應該不會進入這個 if 條件了
      // 其實還是會進入的,因爲定時器的延時
      // 並不是準確的時間,很可能你設置了2秒
      // 但是他需要2.2秒才觸發,這時候就會進入這個條件
      if (remaining <= 0 || remaining > wait) {
        // 如果存在定時器就清理掉否則會調用二次回調
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // 判斷是否設置了定時器和 trailing
        // 沒有的話就開啓一個定時器
        // 並且不能不能同時設置 leading 和 trailing
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };

防抖

防抖動是將多次執行變爲最後一次執行。

/**
 * 防抖函數,返回函數連續調用時,空閒時間必須大於或等於 wait,func 纔會執行
 *
 * @param  {function} func        回調函數
 * @param  {number}   wait        表示時間窗口的間隔
 * @param  {boolean}  immediate   設置爲ture時,是否立即調用函數
 * @return {function}             返回客戶調用函數
 */
export function debounce(func, wait = 200, immediate = false) {
    let timer, context, args

    // 延遲執行函數
    const later = () => setTimeout(() => {
        // 延遲函數執行完畢,清空緩存的定時器序號
        timer = null
        // 延遲執行的情況下,函數會在延遲函數中執行
        // 使用到之前緩存的參數和上下文
        if (!immediate) {
            func.apply(context, args)
            context = args = null
        }
    }, wait)

    // 這裏返回的函數是每次實際調用的函數
    return function (...params) {
        // 如果沒有創建延遲執行函數(later),就創建一個
        if (!timer) {
            timer = later()
            // 如果是立即執行,調用函數
            // 否則緩存參數和調用上下文
            if (immediate) {
                func.apply(this, params)
            } else {
                context = this
                args = params
            }
            // 如果已有延遲執行函數(later),調用的時候清除原來的並重新設定一個
            // 這樣做延遲函數會重新計時
        } else {
            clearTimeout(timer)
            timer = later()
            context = this
            args = params
        }
    }
}

避免內聯樣式

用 CSS 動畫代替 JavaScript 動畫

服務器gzip壓縮

Web Workers 處理 CPU 密集任務

整理學習自你需要掌握的21個React性能優化技巧

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章