iT邦幫忙

2021 iThome 鐵人賽

DAY 24
0
Modern Web

用30天更加認識 React.js 這個好朋友系列 第 24

Day24-React 效能優化篇-上篇(四個優化效能的技巧)

在 React hook 篇章時我們認識了一些避免 re-render 的 hook,像是 useMemo、useCallback,還有 HOC React.memo。

而在這個篇章中會介紹幾個除了 hook 以外,讓 React 效能更好或是避免效能變差的技巧。

方法 1. Code-Splitting & Dynamic Import

React 專案常常都會搭配著 webpack 使用,而透過 webpack 打包 React 專案後產生一個 bundle.js (或者可以自己修改預設名字)的檔案,裡面包含了許多的 js 程式碼。

當一個專案的規模越龐大時,打包出來的 bundle.js 檔案也會越大,載入網站的時間也會變得更久,但實際上 bundle.js 裡面某些的程式碼會在一開始載入就用上嗎?並不見得。所以我們可以靠 Code-Splitting 的其中一個方式: 以 Dynamic Import 的方式在適當的時機才載入需要的程式碼。

要做 Dynamic Import 有好幾種方式,以下一一介紹:

Dynamic Import 方式 1. JS 的 import() 語法

React 官網 有個段落提到可以使用 import() 進行 Dynamic Import。

範例:

import('/modules/my-module.js')
  .then((module) => {
    // Do something with the module.
  });

Dynamic Import 方式 2. React.lazy & React.Suspense

這兩個是 React 提供的 API,可惜 React 官網 提到 SSR 時不能使用它們。

React.lazy 接受一個 callback function 當作參數,當首次渲染元件時才會 import 該元件的 bundle。

使用範例:

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

接著 React.lazy 回傳的 lazy 元件要放在 Suspense 元件內部,並且 Suspense 的 props fallback 可以放置一個元件,當 Suspense 元件內部的元件渲染完成前,就先渲染 fallback 內的元件。

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

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

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
        <AnotherComponent />
      </Suspense>
    </div>
  );
}

另外,React.lazy & React.Suspense 也可以搭配 React Router 使用。

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

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

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

Dynamic Import 方式 3. React Loadable 套件

除了上面的兩個方式外,我們也可以使用 React Loadable 套件 這個套件,如果要做 SSR 就可以使用它來做。

react-loadable 接收了一個物件參數,loader 屬性是一個函式,會 Dynamic Import 需要的元件,當 Dynamic Import 元件還沒渲染完成前,就先渲染 loading 屬性的元件。

import Loadable from 'react-loadable';

const Loading = () => {
  return <div>Loading......</div>
};

const LoadableComponent = Loadable({
  loader: () => import('./my-component'),
  loading: Loading,
});

export default class App extends React.Component {
  render() {
    return <LoadableComponent/>;
  }
}

方法 2. 避免元件多次 mount & unmount

如果要隱藏和顯示元件的話可以透過 CSS 的 opacity 去控制,而不是透過 return 不同的元件去做隱藏和顯示。

// 如果 state view 頻繁變更,元件也會頻繁的 mount & unmount
function Component(props) {
  const [view, setView] = useState('view1');
  return view === 'view1' ? <SomeComponent /> : <AnotherComponent />  
}

// 改善
const visibleStyles = { opacity: 1 };
const hiddenStyles = { opacity: 0 };
function Component(props) {
  const [view, setView] = useState('view1');
  return (
    <React.Fragment>
      <SomeComponent style={view === 'view1' ? visibleStyles : hiddenStyles}>
      <AnotherComponent style={view !== 'view1' ? visibleStyles : hiddenStyles}>
    </React.Fragment>
  )
}

方法 3. 避免重新建立函式&物件造成 re-render

因為在每次元件 re-render 時,匿名函式都要被重新分配記憶體位置,行內樣式也是一樣的原理。

import React from 'react';

// bad
// 父元件為 class component
class Page extends React.Component {
  render() {
    return <Button onClick={() => {
      this.setState({ isClicked: true })
    }} />
  }
}

// 父元件為 function component
const Page = () => {
  return <Button onClick={() => {
    setIsClicked(true)
  }} />
}

// good
// 父元件為 class component
class Page extends React.Component {
  handleClick = () => {
    this.setState({ isClicked: true })
  }
  
  render() {
    return <Button onClick={this.handleClick} />
  }
}

// 父元件為 function component
const Page = () => {
  const handleClick = React.useCallback(() => {
    setIsClicked(true)
  }, [])
  
  return <Button onClick={handleClick} />
}

剩下的範例也是相同的原理:

React element 用 useMemo 優化,不然每次 re-render 都會建立新的 element。

// bad
const Page = () => {
  return <Button content={<p>Click me!</p>} />
}

// good
const Page = () => {
  const content = React.useMemo(() => <p>Click me!</p>, [])

  return <Button content={content} />
}

方法 4. 用 map() 渲染列表時,避免用 map 的 index 當作 key 值

這點還蠻好理解的,若列表裡面的項目有改變,react 會透過 key 的值去檢查哪些項目要做重新渲染。

如果用 map 的 index 當 key,列表的第一個項目被移除,所有列表項目(可以想成陣列裡面的元素)都往前遞補,原本的第二個項目變成新的第一個項目,項目內容當然會改變導致整個列表重新渲染。

參考文章:

6 tips for better React performance

5 Tips to Improve the Performance of Your React Apps

如何優化你的 React App


上一篇
Day23-React Life Cycle 篇-下篇(Updating & Unmounting & Error handling & Render Phase & Commit Phase)
下一篇
Day25-React 效能優化篇-下篇(介紹 React Profiler)
系列文
用30天更加認識 React.js 這個好朋友32

尚未有邦友留言

立即登入留言