延續昨天介紹的 Code Splitting,今天要介紹如何使用動態匯入來拆分程式碼,會以 React 的動態匯入來說明。
在介紹動態匯入前,我們先看一個沒有動態匯入的案例,假設有個商品詳細頁的元件程式碼如下。
// ProductDetailPage.js
import { useState } from "react";
import ProductInfoTabs from "./ProductInfoTabs";
import RelatedProducts from "./RelatedProducts";
const ProductDetailPage = () => {
const [activeTab, setActiveTab] = useState("description");
const handleTabChange = (tab) => {
setActiveTab(tab);
};
return (
<div className="product-detail">
<h1 className="product-name">優雅雙層蕾絲V領上衣</h1>
<div className="product-image">
<img src="https://via.placeholder.com/400" alt="商品圖片" />
</div>
<p className="product-price">NT$ 1,200</p>
<button className="add-to-cart">加入購物車</button>
<ProductInfoTabs
activeTab={activeTab}
handleTabChange={handleTabChange}
/>
<RelatedProducts />
</div>
);
};
export default ProductDetailPage;
在 ProductInfoTabs
內有分幾個 tab 來顯示商品的不同詳細資訊,可以看到我們用靜態匯入的方式 import
了 3 個 tab 子元件:ProductDescription
、ProductDetails
和 ShippingInfo
,這些元件都會被一起打包到初始 bundle 內,然而其實像 ProductDetails
和 ShippingInfo
在一開始載入頁面時使用者是不會見到的,不需要一起被打包到初始 bundle 內,我們只需要在使用者點擊該 tab 的時候再動態匯入元件即可。因此我們可將 ProductDetails
和 ShippingInfo
改使用動態匯入。
// ProductInfoTabs.js
import ProductDescription from "./ProductDescription";
import ProductDetails from "./ProductDetails";
import ShippingInfo from "./ShippingInfo";
const tabs = [
{ name: "description", label: "商品描述", component: <ProductDescription /> },
{ name: "details", label: "額外細節", component: <ProductDetails /> },
{ name: "shipping", label: "運送與付款資訊", component: <ShippingInfo /> },
];
const ProductInfoTabs = ({ activeTab, handleTabChange }) => {
const activeTabComponent = tabs.find(
(tab) => tab.name === activeTab
)?.component;
return (
<div className="product-info-tabs">
<div className="tabs">
{tabs.map((tab) => (
<button
key={tab.name}
className={`tab ${activeTab === tab.name ? "active" : ""}`}
onClick={() => handleTabChange(tab.name)}
>
{tab.label}
</button>
))}
</div>
<div className="tab-content">{activeTabComponent}</div>
</div>
);
};
export default ProductInfoTabs;
完整程式碼請見連結。
在 React 中要使用動態匯入,可搭配使用 React 提供的 lazy
和 Suspense
:
lazy
用來告訴 React 要動態載入元件Suspense
用來包住會動態載入的元件,當元件還沒載入好時,可先顯示 fallback
元件,例如fallback
顯示 loading ,用來告訴使用者此元件正在載入中將上面的 ProductInfoTabs
加上動態匯入,改寫如下。
// Step 1: 從 react 匯入 lazy 和 Suspense
import { lazy, Suspense } from "react";
import ProductDescription from "./ProductDescription";
// Step 2: 使用 lazy 來動態匯入元件,用 lazy 來動態匯入的元件不會被打包到初始 bundle 內
const ProductDetails = lazy(() => import("./ProductDetails"));
const ShippingInfo = lazy(() => import("./ShippingInfo"));
const tabs = [
{ name: "description", label: "商品描述", component: <ProductDescription /> },
{ name: "details", label: "額外細節", component: <ProductDetails /> },
{ name: "shipping", label: "運送與付款資訊", component: <ShippingInfo /> },
];
const ProductInfoTabs = ({ activeTab, handleTabChange }) => {
const activeTabComponent = tabs.find(
(tab) => tab.name === activeTab
)?.component;
return (
<div className="product-info-tabs">
<div className="tabs">
{tabs.map((tab) => (
<button
key={tab.name}
className={`tab ${activeTab === tab.name ? "active" : ""}`}
onClick={() => handleTabChange(tab.name)}
>
{tab.label}
</button>
))}
</div>
{/* Step 3: 用 Suspense 包住會動態載入的元件,當 Suspense 裡面的 children 元件有還沒載入好的,就會先顯示 fallback 傳入的 element,以這裡來說就是顯示 dynamic loading... */}
<Suspense fallback={<h2>dynamic loading...</h2>}>
<div className="tab-content">{activeTabComponent}</div>
</Suspense>
</div>
);
};
export default ProductInfoTabs;
將 ProductDetails
和 ShippingInfo
改為動態匯入,可減少初始 bundle 大小,加快初始載入速度,而 Suspense
的 fallback
可以讓使用者知道正在載入,讓使用者知道應用程式沒有卡住,改善使用者體驗。
完整程式碼請見連結。
lazy
使用動態匯入要注意的是,要在元件外部呼叫 lazy
,而非在元件內部呼叫,若在元件內部呼叫 lazy
動態匯入元件,會在 re-render 時重置(reset)動態匯入元件的所有狀態(state)。
import { lazy } from 'react';
function ProductInfoTabs() {
// ⛔️ 這會導致所有狀態在 re-render 時重置
const ProductDetails = lazy(() => import("./ProductDetails"));
// ...
}
應該在元件外部呼叫 lazy
動態匯入元件。
import { lazy } from 'react';
// ✅ 在元件外部定義 lazy component
const ProductDetails = lazy(() => import("./ProductDetails"));
function ProductInfoTabs() {
// ...
}
lazy
目前只支援 default exportReact 的 lazy
目前只支援 default export 的元件,如果要用 lazy
動態匯入一個具名匯出(named export)的元件,是無法直接使用 lazy
的,要多一個中介的檔案來 default export 該元件才可以。
// MyComponent.js
// 原始要動態匯入的元件,使用 named export 而非 default export
export const MyComponent = () => {
return (
// ...
);
};
新增一個中介檔案來把元件轉成 default exports,這樣才能讓 tree shaking 發揮作用:
// proxyMyComponent.js
export { MyComponent as default } from './MyComponent.js';
要使用 lazy
匯入元件的地方,需從 proxyMyComponent.js
的地方來匯入:
// App.js
import { lazy } from "react";
const MyComponent = lazy(() => import("./proxyMyComponent"));
export default function App() {
//...
}
補充:tree shaking
tree shaking 是一種在 JavaScript 應用程式中,移除未使用程式碼的技術,它可以確保最後打包的 bundle 不會包含不必要的程式碼,以減少 bundle 的檔案大小。(ref: Day15 X Tree Shaking)
在 Module 模式的文章內已介紹過動態匯入的類型,依照匯入的時機可分為:
當使用者點擊、互動時,再動態載入元件,稱為互動匯入(Import on Interaction),是透過使用者互動而觸發的元件匯入。
以上述 ProductInfoTabs
內的 ProductDetails
和 ShippingInfo
來說,就屬於互動匯入,當使用者點擊對應的 tab 時,才匯入該特定元件。
不需在初始頁面載入還不可見的元件,而是當使用者往下滾動、元件變成可見時,才觸發動態匯入,稱為可見性匯入(Import on Visibility),常見的例如圖片在可視區域(viewport)內時,才動態載入圖片。
在 React 中一樣可利用 Intersection Observer API 來確認元件是否在可視範圍內。
某些特定資源可能只在特定頁面上才需要,可基於路由來拆分、動態匯入元件,可搭配 React Suspense
和 react-router 來根據路由載入元件。
import React, { Suspense } from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Navbar from "./Navbar";
// 使用 React.lazy 動態匯入元件
const Home = React.lazy(() => import("./Home"));
const Setting = React.lazy(() => import("./Setting"));
const Contact = React.lazy(() => import("./Contact"));
function App() {
return (
<Router>
<Navbar />
{/* 使用 Suspense 包住會動態匯入的元件,以顯示載入狀態 */}
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/setting" element={<Setting />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
</Router>
);
}
export default App;
完整程式碼請見連結。
使用動態匯入的優點如下:
使用動態匯入的缺點如下:
lazy
搭配 Suspense
),對於初階開發者來說,可能需要花較多時間來理解和撰寫這些程式碼