[铁人赛 Day05] React 中的 Code splitting(代码分离)方法

什麽是 Code splitting?为什麽要做 Code splitting?

如果你的网站是用 Create React App, Next.js, Gatsby 或者其他类似工具写的,那你有很大的机率,会使用 bundlers 来打包你的网站。

随着我们的网站成长,功能变多、内部逻辑越来越复杂,CSS、JavaScript 的档案或 bundles 越来越大包,如果你有使用第三方套件,这个问题会更严重。当你的网站运行时,下载一大包的档案是个负担,拉长网站的 JavaScript execution time,这就是 Code splitting 方法上用场的地方。

如何实现 Code splitting?

与其一次下载一整包档案,不如等到这些 code 会被使用上时(或者说当这些画面,进入视窗范围时),才去下载,藉此提升网站的 initial load performance 。大致上的做法是:把 code 分成数个档案,常见 bundlers 例如 Webpack、Browserify 都有支援这种做法,他们可以创造出数个 bundle 档(原本是只有一包),然後采用一种叫做 Lazy load 的策略做 bundle 的动态载入。

在开始之前:来个自我检测

你的专案可能有 JavaScript execution time 过长的问题吗?让我们拿随意一个网站来做检测,按照下面的红框处,打开你的 Lighthouse 并按下 Generate Report。

https://ithelp.ithome.com.tw/upload/images/20210920/20140045j2auOmnFaB.png

Report 看起来会像这样:

https://ithelp.ithome.com.tw/upload/images/20210920/2014004580pHK1p2HY.png

你可以往下滑,找找看有无「Reduce JavaScript execution time」的建议,点选 Learn more,可以看到 Google 的 web.dev 提供的更多优化方法,code splitting 是其中一种。

https://ithelp.ithome.com.tw/upload/images/20210920/20140045c2YdCyTrkc.png

React 提供的 Code splitting 方法

  1. dynamic import

当 Webpack 读到 dynamic import 的指令时,会自动针对你的网站做 code-splitting。dynamic import 使用 then 方法,当网站需要用到这段 code 的时候,去 call then 方法里面的函示。

// 原本的 import 看起来像
import { add } from './math';
console.log(add(16, 26));
// dynamic import 看起来像
import("./math").then(math => {
  console.log(math.add(16, 26));
});

如果你是自行设定 Webpack 的用户(没有使用 Create React App 、Next.js...类似工具),需要额外的设定来启用这个功能。

Webpack 设定官方文件:https://webpack.js.org/guides/code-splitting/

React team 的 Dan Abramov 提供了一套范例,你的 Webpack 设定看起来会像这样 :https://gist.github.com/gaearon/ca6e803f5c604d37468b0091d9959269

如果你还搭着 Babel 一起用,要确认 Bable 有办法正确的 parse,你可以参考这个套件:https://developer.mozilla.org/en-US/docs/Glossary/Code_splitting

  1. React.lazy

注意:此方法不适用於 server-side rendering 的网站,SSR 须参考 loadable-components 这个套件。

React.lazy 让你用更习惯的方法来实作 dynamic import 。在下面的 code 里,当 OtherComponent 被初次 render 时,才会载入这包 bundle。

实作方法是在 React.Lazy 传入一个匿名函示,呼叫 dynamic import() 的方法,这个动作会 return 出一个 Promise ,这个 Promise 会 resolve 出一个 module,这个 module 内,是一个带有 React component 的 default export。

// 使用 React.Lazy 的动态载入
const OtherComponent = React.lazy(() => import('./OtherComponent'));

如果 React 准备要 render component 了,相关联的 code 还没下载好,该怎麽办?要将 lazy component render 出来,必须包在 <Suspense> 元件内,而这个 <Suspense> 可以接收 fallback 设定,表示 code 尚未到位时,画面上应该显示什麽提示。

import React, { Suspense } from 'react';

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

function MyComponent() {
  return (
    <div>
			// 当底下的 OtherComponent 尚未准备完成,画面上会显示 Loading...
			// fallback prop 也可以是 React elements
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

只要包在 lazy component 外面,你可以把 <Suspense> 元件放在任何位置,甚至也可以用一个 <Suspense> 元件来包住多个 lazy component。

import React, { Suspense } from 'react';

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

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

如果今天不是「还没载入完成」的问题,而是「载入失败」呢?你可以客制一个 Error Boundary 来处理这个问题。ErrorBoundary 该如何实作,又能够控制到什麽程度?请见下一篇文章。

import React, { Suspense } from 'react';
import MyErrorBoundary from './MyErrorBoundary';

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

const MyComponent = () => (
  <div>
		// 使用 ErrorBoundary 来包住 lazy component
    <MyErrorBoundary>
      <Suspense fallback={<div>Loading...</div>}>
        <section>
          <OtherComponent />
          <AnotherComponent />
        </section>
      </Suspense>
    </MyErrorBoundary>
  </div>
);

虽然 React.Lazy 提供的方法看来简单,要选择运用在哪里,却是困难的事情,毕竟如果 split bundles 位置挑选不好,可能会影响使用者体验。

一个比较好的选择,是在页面切换的时候。虽然页面切换通常有一些相依性(dependencies),但使用者也已经习惯切换页面时的几秒延迟。

React.lazyReact Router 的合作方式也很简洁,可以直接将 lazy component 传入 Route 元件,并且把 <Suspense> 包在 switch 之外即可。

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>
);

参考资料:

https://web.dev/code-splitting-suspense/

https://reactjs.org/docs/code-splitting.html

https://blog.logrocket.com/code-splitting-in-react-an-overview/


<<:  [Golang] Custom Type Declarations and Struct

>>:  Proxmox VE 安装虚拟机:Windows 10 (一)

冒险村30 - Handle API response with value objects

30 - Handle API response with value objects 本篇将介绍撰...

CSS基础介绍(3)

来轻松聊聊 终於来到基础CSS的最後一篇,这次要分享的是CSS的变量。 想像一个情境,你正在负责一个...

自动化测试,让你上班拥有一杯咖啡的时间 | Day 27 - 学习 cypress window 的用法

此系列文章会同步发文到个人部落格,有兴趣的读者可以前往观看喔。 今天要跟大家分享当网站有用到 Ja...

Postman pre-request script & tests

本质上是一样的东西,只是一个是在 request 前执行、一个是在收到 response 後执行,分...

资安学习路上-picoCTF 解题(Reverse)2

4.speeds and feeds Google後发现CNC 的language是"G-...