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


<<:  【24】如果把 Dropout 放在 CNN 之後会发生什麽事

>>:  Unity与Photon的新手相遇旅途 | Day23-Photon房间场景

第八天:用 Docker 运行 Gradle

昨天介绍的全手动安装是对系统掌控的一种极端,而今天要介绍的,是对系统洁癖的另一种极端。假如你受够了要...

软件工程师从新手到高手的流程

“工慾善其事必先利其器”:利器阶段(把技术放入自己的工具箱): Step1:协助公司(中小型公司)维...

Day19 :【TypeScript 学起来】More on Functions

我们在前面 Day09 , 有简单讨论到 function,这篇就会来看一些更深入 functio...

因边界网关协议 ( BGP) 路由配置错误导致 DNS 故障而遭受服务中断,防止此事件的最佳对策-对配置更改实施两人控制

-一般问题解决过程 如果配置由其他工程师仔细检查,则对配置更改强制执行两人控制可能会避免该事件。这...

创作App-Xcode资料库

有了基础的注册系统後,建立资料库来连接系统,用於储存用户、使用者的帐号与密码、等级等,区分版本功能。...