什麼是 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。
Report 看起來會像這樣:
你可以往下滑,找找看有無「Reduce JavaScript execution time」的建議,點選 Learn more,可以看到 Google 的 web.dev 提供的更多優化方法,code splitting 是其中一種。
React 提供的 Code splitting 方法
當 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
注意:此方法不適用於 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.lazy
與 React 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/