iT邦幫忙

2021 iThome 鐵人賽

DAY 5
1
Modern Web

React 從 0.5 到 1系列 第 5

[鐵人賽 Day05] React 中的 Code splitting(代碼分離)方法

  • 分享至 

  • xImage
  •  

什麼是 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/


上一篇
[鐵人賽 Day04] 如何提升你的 React 網站易用性?(Web Accessibility)(下)- Mouse and pointer events、Development Tools
下一篇
[鐵人賽 Day06] React 中如何攔截網站 Runtime 錯誤?- Error boundaries
系列文
React 從 0.5 到 115
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0

我要留言

立即登入留言