React組件設(shè)計(jì)之性能優(yōu)化篇

前言

由于筆者最近在開發(fā)中遇到了一個(gè)重復(fù)渲染導(dǎo)致子組件狀態(tài)值丟失的問題,因此關(guān)于性能優(yōu)化做了以下的分析,歡迎大家的交流

我們在日常的項(xiàng)目開發(fā)中往往會(huì)把頁面拆分成一個(gè)個(gè)的組件,通過拼裝的方式來實(shí)現(xiàn)整體的頁面效果,所以與其說去優(yōu)化 React,不如聚焦在現(xiàn)有的組件中,思考??如何去設(shè)計(jì)一個(gè)組件才能提高他的性能,從而提高整個(gè)項(xiàng)目的性能以及交互的流暢性。

回顧

在我們初學(xué) React 的時(shí)候相信大家或多或少運(yùn)行過這樣的 demo:

import React, { useState } from 'react';
import { Button, Space } from 'antd';
import 'antd/dist/antd.css';
import './index.css';

const Parent: React.FC = () => {
  const [count, setCount] = useState(0);
  return (
    <Space direction="vertical">
      {count}
      <Button type="primary" onClick={() => setCount(count + 1)}>
        count + 1
      </Button>
      <Children />
    </Space>
  );
};

const Children: React.FC = () => {
  console.log('更新了子組件');
  return <div>這是子組件</div>;
};

export default Parent;

代碼很少就是個(gè)簡單的父組件嵌套子組件的情況,我們測試運(yùn)行也沒啥問題,大多數(shù)時(shí)候我們可能也就這么開發(fā)了。
但是我們觀察下,每次當(dāng)我們點(diǎn)擊按鈕進(jìn)行 count + 1 時(shí)會(huì)更新子組件,這一點(diǎn)從控制臺(tái)打印的信息可以看出。雖然 React 中有 diff 算法決定是否需要切實(shí)更新 DOM 元素,但是其內(nèi)部的定義的一些函數(shù)還是會(huì)執(zhí)行,而且 diff 會(huì)遍歷整棵 virtualDOM 樹也會(huì)有一定的性能消耗,那能不能優(yōu)化下這個(gè)勒。

性能優(yōu)化實(shí)踐

由于 React 中的組件分為 Function 組件和 Class 組件,優(yōu)化的手段根據(jù)其特性不同也分為兩類

Function 組件

React.memo

React.memo 是 React 提供的一個(gè)高階組件,用于優(yōu)化組件的性能。它可以在某些情況下避免不必要的組件重新渲染,從而提高應(yīng)用程序的性能。其使用方式分為兩種:

  • 基礎(chǔ)使用
    函數(shù)組件直接包裹 React.memo 默認(rèn)使用淺層比較。
  • 高階使用
    如果需要更精確地控制何時(shí)重新渲染組件,可以通過傳遞第二個(gè)參數(shù)給 React.memo 來指定自定義的比較函數(shù)。這個(gè)比較函數(shù)接收兩個(gè)參數(shù),分別是前一次的 props 和當(dāng)前的 props ,返回一個(gè)布爾值表示是否需要重新渲染組件
import React from 'react';

const areEqual = (prevProps, nextProps) => {
  // 自定義比較邏輯
  // 返回 true 表示兩個(gè) props 相等,不需要重新渲染
  // 返回 false 表示兩個(gè) props 不相等,需要重新渲染
  return prevProps.value === nextProps.value;
};

const MyComponent = React.memo((props) => {
  console.log('Rendering MyComponent');
  return <div>{props.value}</div>;
}, areEqual);

使用useCallback

useCallback 是 React 中的一個(gè) Hook,用于優(yōu)化性能和避免不必要的渲染。它主要用于創(chuàng)建一個(gè)穩(wěn)定的回調(diào)函數(shù),并在依賴項(xiàng)未發(fā)生變化時(shí)緩存該函數(shù)。
示例代碼:

import React, { useState, useCallback, useEffect } from 'react';

const MyComponent = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    handleClick();
  }, [handleClick]);

  // 使用 useCallback 緩存回調(diào)函數(shù) handleClick
  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
};

注意??:并不是必需的,它主要用于解決特定的性能問題。在大多數(shù)情況下,使用普通的函數(shù)定義也是有效的。只有在性能優(yōu)化成為問題時(shí),才需要考慮使用 useCallback。過度使用該useCallback會(huì)導(dǎo)致內(nèi)存占用增加(每個(gè)緩存的回調(diào)函數(shù)都會(huì)占用內(nèi)存),代碼復(fù)雜度增加可讀性變差并且難以維護(hù)。
可以遵循以下原則:

  • 只在需要時(shí)使用:只有在明確的性能問題存在時(shí),或者需要將回調(diào)函數(shù)作為依賴項(xiàng)傳遞給其他 Hooks(如 useEffectuseMemo)時(shí),才使用 useCallback
  • 明確指定依賴項(xiàng):確保正確指定 useCallback 的依賴項(xiàng)數(shù)組,以確保緩存的回調(diào)函數(shù)在依賴項(xiàng)未發(fā)生變化時(shí)不會(huì)重新創(chuàng)建

使用useMemo

它用于在組件渲染過程中進(jìn)行記憶化計(jì)算,以避免不必要的重復(fù)計(jì)算,提高應(yīng)用的性能。
使用場景:

  • 計(jì)算昂貴的計(jì)算結(jié)果:涉及到需要執(zhí)行昂貴的計(jì)算或處理大量數(shù)據(jù)的情況下,可以使用 useMemo 將計(jì)算結(jié)果緩存起來
  • 避免不必要的渲染:某個(gè)組件的渲染結(jié)果僅依賴于特定的輸入?yún)?shù),并且這些參數(shù)沒有發(fā)生變化時(shí),可以使用 useMemo 緩存該組件的輸出,避免不必要的重新渲染
import React, { useMemo } from 'react';

const MyComponent = ({ data }) => {
  // 使用 useMemo 緩存結(jié)果
  const processedData = useMemo(() => {
    // 執(zhí)行昂貴的計(jì)算或處理邏輯
    // 這里只是一個(gè)簡單的示例,實(shí)際場景可能更復(fù)雜
    console.log('Processing data...');
    return data.map(item => item * 2);
  }, [data]); // 依賴項(xiàng): 當(dāng) data 發(fā)生變化時(shí)重新計(jì)算

  return (
    <div>
      {/* 渲染使用 useMemo 緩存的結(jié)果 */}
      <ul>
        {processedData.map(item => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
};

Class 組件

巧用PureComponent

!!僅支持React 15.3及以上版本
PureComponent 是繼承自 React.Component 的一個(gè)子類,它額外實(shí)現(xiàn)了shouldComponentUpdate 方法,并通過對(duì)組件的 props 和 state 進(jìn)行淺層比較來確定是否需要重新渲染組件。
使用方式:

class App extends React.PureComponent

如果 props 和 state 沒有發(fā)生改變,就不會(huì)進(jìn)入 render 節(jié)點(diǎn),省去了生成 Virtual DOM 和 Diff 的過程。
低版本可以使用PureRenderMixin,使用淺比較來決定是否應(yīng)該觸發(fā)組件的重新渲染。它會(huì)自動(dòng)為組件添加一個(gè) shouldComponentUpdate 方法,該方法會(huì)比較新的 propsstate 與當(dāng)前的 propsstate,并根據(jù)比較結(jié)果決定是否重新渲染組件

import PureRenderMixin from 'react-addons-pure-render-mixin';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
  }

  // 組件的其他方法和生命周期函數(shù)
  // ...
}

合理使用shouldComponentUpdate

如果想對(duì)渲染進(jìn)行更加細(xì)微的控制,或者是對(duì)引用類型進(jìn)行渲染控制我們可以使用shouldComponentUpdate,通過返回 true 進(jìn)行更新, false 阻止不必要的更新。

class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // 對(duì)比新舊屬性和狀態(tài)
    if (this.props.value === nextProps.value && this.state.count === nextState.count) {
      return false; // 屬性和狀態(tài)相同,不需要重新渲染
    }
    return true; // 需要重新渲染
  }

  render() {
    return <div>{this.props.value}</div>;
  }
}

特別注意??:合理使用,手動(dòng)實(shí)現(xiàn) shouldComponentUpdate 可能會(huì)增加代碼的復(fù)雜性,并且過度使用它可能會(huì)導(dǎo)致更多的維護(hù)問題。只有在確實(shí)需要優(yōu)化性能時(shí),才建議使用它。

在構(gòu)造函數(shù)中綁定this

當(dāng)我們在類組件中綁定類方法通過需要綁定他的 this 指向

// 方式一
render() {
  return <Button onClick={this.handleClick.bind(this)}>測試按鈕</Button>
}

// 方式二
constructor() {
  super();
  this.handleClick = this.handleClick.bind(this);
}

雖然用起來是一樣的,但是第一種方式在 render 的時(shí)候,每次會(huì) bind this 生成新的函數(shù)實(shí)例,而第二種只會(huì)執(zhí)行一次。

React中其他的優(yōu)化手段

組件卸載時(shí)的清理

組件中注冊的全局的監(jiān)聽器、定時(shí)器等,需要在組件卸載的時(shí)候進(jìn)行清理,防止后續(xù)的執(zhí)行影響性能以及內(nèi)存泄露等問題

  • Class 組件:componentWillUnmount
  • Function 組件:useEffect return
import React, { useState, useEffect } from 'react';

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 定義定時(shí)器
    const timer = setInterval(() => {
      setCount(count => count + 1);
    }, 1000);

    const handleOnResize = () => {
    console.log('Window resized');
    }

    // 定義監(jiān)聽器
    const listener = window.addEventListener('resize', handleOnResize);

    // 在組件卸載時(shí)清除定時(shí)器和監(jiān)聽器
    return () => {
      clearInterval(timer);
      window.removeEventListener('resize', handleOnResize);
    };
  }, []);

  return (
    <div>
      <p>{count}</p>
    </div>
  );
}

export default Timer;

使用lazy進(jìn)行組件懶加載

React版本支持16.6及以上版本
低版本可以考慮使用第三方庫(如react-loadable)來實(shí)現(xiàn)類似的懶加載效果

通過組件懶加載可以將代碼分割成更小的塊,并且只有在需要時(shí)才會(huì)被加載
當(dāng)用戶訪問某個(gè)特定頁面時(shí),只有與該頁面相關(guān)的代碼會(huì)被下載和執(zhí)行,而其他代碼則不會(huì)被加載。這樣可以使應(yīng)用程序更快地啟動(dòng),并減少頁面響應(yīng)延遲。

import React, { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));

function App() {
  return (
    <Router>
      <Suspense fallback={<div>Loading...</div>}>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/about" component={About} />
        </Switch>
      </Suspense>
    </Router>
  );
}

export default App;

在React中我們通常使用 lazySuspense 互相配合的方式進(jìn)行懶加載,使用 lazy 方法來懶加載 HomeAbout 組件,使用Suspense可以在組件加載完成之前顯示一個(gè)自定義的加載指示器或占位符,從而提高用戶體驗(yàn)。

使用React Fragment減少額外節(jié)點(diǎn)的渲染

!!僅支持React 16.2及以上版本
React Fragment 允許你在 React 組件中返回多個(gè)元素而不需要添加額外的根節(jié)點(diǎn),比如在某些情況下你的組件返回的是一個(gè) list 的元素集合而他的根元素在父組件中,你想要的結(jié)構(gòu)是根節(jié)點(diǎn)內(nèi)直接元素的集合而不想再包裹一層,可以使用這種方式解決

javascript
import React, { lazy, Suspense } from 'react';

function TdList() {
  return (
    <React.Fragment>
      <td>Hello, World!</td>
      <td>This is a paragraph.</td>
    </React.Fragment>
  );
}

function Table() {
  return (
    <table>
      <tr>
    <TdList />
      </tr>
    </table>
  );
}

除此以外還具有一下優(yōu)勢:

  • 更清晰的代碼結(jié)構(gòu):以片段的形式包裹元素可讀性更高,避免了成片的div
  • 減少 DOM 層級(jí):減少渲染出來的 DOM 層級(jí),從而提高性能
  • 更符合預(yù)期:更容易使我們按照預(yù)期呈現(xiàn)出想要表達(dá)的組件特別是在便利的時(shí)候,而不會(huì)引起元素的父子關(guān)系問題

有時(shí)候我們也會(huì)使用<></>,被稱為空標(biāo)簽或者隱式 Fragment,其用法和Fragment相同,唯一的區(qū)別在于空標(biāo)簽不能添加任何屬性,而后者可以,比如說在某些場景下需要給父元素增加key值。此時(shí)只能使用Fragment

減少使用通過內(nèi)聯(lián)函數(shù)綁定事件

當(dāng)我們在 React 中使用內(nèi)聯(lián)函數(shù)時(shí),每次重新 render 將導(dǎo)致生成新的函數(shù)實(shí)例從而為元素綁定新的函數(shù),在非嵌套的組件使用時(shí)影響不大,但是如果存在嵌套組件并且該內(nèi)聯(lián)函數(shù)是作為 props 傳遞給子組件時(shí)將會(huì)導(dǎo)致子組件重新渲染,即使內(nèi)聯(lián)函數(shù)里的代碼相同的情況下。

  • Class 組件:將內(nèi)聯(lián)函數(shù)定義為類方法傳遞給子組件
  • Function 組件:useCallback進(jìn)行緩存

// bad
import React, { useCallback } from 'react';

function MyComponent() {
  return (
    <button 
    onClick={() => { 
      // 處理點(diǎn)擊事件 
    }}
     >
    Click me
    </button>
  );
}

// good
import React, { useCallback } from 'react';

function MyComponent() {
  const handleClick = useCallback(() => {
    // 處理點(diǎn)擊事件
  }, []);

  return (
    <button onClick={handleClick}>Click me</button>
  );
}

使用key提升列表的渲染性能

假設(shè)你有一個(gè)需要渲染大量數(shù)據(jù)的列表組件,每個(gè)列表項(xiàng)都是一個(gè)獨(dú)立的子組件。當(dāng)你對(duì)這個(gè)列表進(jìn)行添加、刪除或重新排序操作時(shí),React 需要計(jì)算出哪些子組件需要更新。

import React from 'react';

function MyComponent(props) {
  const data = props.data; // 假設(shè)這是一個(gè)包含大量數(shù)據(jù)的數(shù)組

  return (
    <ul>
      {data.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

未使用 key 的情況下,React 將無法區(qū)分列表項(xiàng)之間的差異,而會(huì)重新渲染整個(gè)列表。使用 key 可以幫助 React 提高性能,它可以通過比較新舊的 key 來確定是否需要更新特定的列表項(xiàng)。這樣,當(dāng)你對(duì)列表進(jìn)行操作時(shí),React 只會(huì)針對(duì)變化的部分進(jìn)行更新。
注意??:盡量不要使用 index 作為 key 值

通用的優(yōu)化手段

可見性加載

在我們?yōu)g覽很多圖像、卡片、列表時(shí)我們往往不會(huì)立馬加載所有的資源,主要是因?yàn)橐粋€(gè)是很浪費(fèi)資源,另一個(gè)就是在不可見區(qū)域內(nèi)也必要進(jìn)行加載。借鑒這個(gè)思想,我們是否可以在組件在視口范圍內(nèi)進(jìn)行加載呢,其余不進(jìn)行加載——IntersectionObserver 或者是一些現(xiàn)有的庫如react-loadable-visibility(通過 react-loadable 按需加載組件 + Intersection Observer API 監(jiān)聽組件的可見性)

import React from "react";
import { Button } from "antd";
import LoadableVisibility from "react-loadable-visibility/react-loadable";

const LoadingComponent = () => <div>Loading...</div>;

const MyComponent = LoadableVisibility({
  loader: () => import("./MyComponent"),
  loading: LoadingComponent
});

const App = () => {
  return (
    <div>
      <h1>My App</h1>
      <MyComponent />
    </div>
  );
};

export default App;

當(dāng) MyComponent 組件進(jìn)入視口時(shí),它們才會(huì)被加載和渲染,而在加載過程中,會(huì)顯示 LoadingComponent 組件作為占位符,需要注意的是,確保在支持 Intersection Observer API 的瀏覽器中進(jìn)行

交互式導(dǎo)入資源

頁面中包含并非立即需要的組件或資源的代碼或數(shù)據(jù),立即加載這些資源將阻塞主線程,而這些功能當(dāng)用戶不去觸發(fā)某些操作是也是用不到的,就可以采用這種方式。
比如我們有個(gè)需求是點(diǎn)擊“滾動(dòng)到頂部”按鈕時(shí)以動(dòng)畫方式滾動(dòng)回頁面頂部,這里我們用到了react-scroll 這個(gè)包,那可以在與按鈕交互時(shí)加載它

handleScrollToTop() {
  import('react-scroll').then(scroll => {
    scroll.animateScroll.scrollToTop({
    })
  })
}

Web Worker

通過 Web Worker 創(chuàng)建多線程的環(huán)境,主線程把一些任務(wù)分配給后者運(yùn)行,不會(huì)阻塞主線程的運(yùn)行,使交互更加流暢。
適用場景:

  • 計(jì)算密集型或高延遲的任務(wù)

虛擬列表

僅渲染可見區(qū)域的 dom 元素而不必要渲染全部,提高渲染性能。可以使用 React-virtualized 或者是 React-window 等包。

總結(jié)

以上就是筆者對(duì)性能優(yōu)化方面的研究??和總結(jié),如果大家在日常開發(fā)中有這樣的訴求可以參考以上幾種方式。

參考資料:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。