iT邦幫忙

2024 iThome 鐵人賽

DAY 23
0
JavaScript

30天的 JavaScript 設計模式之旅系列 第 23

[Day 23] 程式碼拆分(Code Splitting)與動態匯入(Dynamic Import) (2)

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20241007/20168201sCTJBUQBSf.png

延續昨天介紹的 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 子元件:ProductDescriptionProductDetailsShippingInfo,這些元件都會被一起打包到初始 bundle 內,然而其實像 ProductDetailsShippingInfo 在一開始載入頁面時使用者是不會見到的,不需要一起被打包到初始 bundle 內,我們只需要在使用者點擊該 tab 的時候再動態匯入元件即可。因此我們可將 ProductDetailsShippingInfo 改使用動態匯入。

// 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 提供的 lazySuspense

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

ProductDetailsShippingInfo 改為動態匯入,可減少初始 bundle 大小,加快初始載入速度,而 Suspensefallback 可以讓使用者知道正在載入,讓使用者知道應用程式沒有卡住,改善使用者體驗。
完整程式碼請見連結

在元件外部使用 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 export

React 的 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 內的 ProductDetailsShippingInfo 來說,就屬於互動匯入,當使用者點擊對應的 tab 時,才匯入該特定元件。

可見性匯入

不需在初始頁面載入還不可見的元件,而是當使用者往下滾動、元件變成可見時,才觸發動態匯入,稱為可見性匯入(Import on Visibility),常見的例如圖片在可視區域(viewport)內時,才動態載入圖片。
在 React 中一樣可利用 Intersection Observer API 來確認元件是否在可視範圍內。

基於路由動態匯入

某些特定資源可能只在特定頁面上才需要,可基於路由來拆分、動態匯入元件,可搭配 React Suspensereact-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;

完整程式碼請見連結

優點

使用動態匯入的優點如下:

  • 減少初始載入時間:動態匯入元件可以將應用程式拆分為多個較小的 bundle,依照需求再動態載入元件,減少初始 bundle 的大小,因而可減少初始載入時間
  • 可延後並依需求載入:可以延遲載入不會被立即需要的元件,在需要時才載入特定模組或元件,讓應用程式的靈活性更高
  • 提高應用程式效能:避免一次性載入過多資源,減少不必要的資源消耗,可提高應用程式的效能

缺點

使用動態匯入的缺點如下:

  • 影響開發體驗:動態匯入可能導致開發時較難追蹤錯誤,且需要的語法較多(需使用 lazy 搭配 Suspense),對於初階開發者來說,可能需要花較多時間來理解和撰寫這些程式碼
  • 過度使用動態匯入可能會增加檔案大小:並非所有元件或頁面都需要使用動態匯入,因為拆分元件時,也會需要額外的程式碼(glue code)來讓不同檔案的程式可以順利連接,而這些用來連接的程式碼也會增加檔案容量,對於原本檔案容量就不大的元件來說,使用動態匯入可能會增加更多程式碼,比不使用動態匯入還增加檔案大小
  • 潛在的效能消耗:過多的小 bundle 可能會增加請求數量,導致瀏覽器需要多次向伺服器請求小 bundle,當過多請求時,可能會影響應用程式效能

Reference


上一篇
[Day 22] 程式碼拆分(Code Splitting)與動態匯入(Dynamic Import) (1)
系列文
30天的 JavaScript 設計模式之旅23
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言