iT邦幫忙

2025 iThome 鐵人賽

DAY 26
0
Modern Web

從 React 學 Next.js:不只要會用,還要真的懂系列 第 26

【Day 26】Next.js 的 JavaScript 載入優化方式 - next/dynamic

  • 分享至 

  • xImage
  •  

昨天看過圖片的優化方式,今天接著再來看看載入 JavaScript 的優化方式。在正式來看為什麼需要額外做一些動作來優化 JavaScript 的載入之前,我們先來看一個實際例子。

觀察實際案例

https://i.imgur.com/vBEVtfV.gif

我們在這一個畫面中,可以透過一個按鈕顯示出一個圖表內容。這樣的畫面看起來沒有什麼問題,但是當畫面內的內容變多,影響到需要載入的 JavaScript 的檔案大小,就有可能會出現畫面載入太慢的狀況。

我們先把原本的畫面改成一個比較單純,只有文字,但沒圖表的頁面,觀察一下 build 完後,會在 client 端被下載來使用的 JavaScript 大小。
https://ithelp.ithome.com.tw/upload/images/20250928/20130914sSuUmhO47Y.png

這裡可以看到首頁的路由在瀏覽器上會下載的 JavaScript 檔案 Size 只有 650 B,首次進入頁面需要載下的 JavaScript 大小是 102 KB。

我們再來看看把 Chart 放回去之後,build 後的檔案大小會有什麼樣的變化。
https://ithelp.ithome.com.tw/upload/images/20250928/20130914WELyQtESma.png
當我們把 Chart 放回畫面後,可以發現不管是專屬於首頁的 JavaScript 檔案,還是首次進入頁面需要下載的 JavaScript 大小就變大了,甚至變成了原本的兩倍。

這時候有人可能會有一個疑問,那就是「畫面上要顯示的內容變多了,JavaScript 大小當然會變多啊!不是很正常嗎?」。

沒錯!這不是一個什麼很神奇或是很異常的現象,但是大家可以仔細思考一下,剛剛我們先拿掉的 Chart 內容,是一進入畫面就需要顯示的內容嗎?如果不是一進入頁面就需要使用,或是需要看到的內容,是不是可以不要在一開始進入畫面就載入相關的 JavaScript 呢?當專案內容越來越龐大,如果可以把需要載入的一大坨 JavaScript,像我們把畫面拆小一樣,拆成一小包一小包的檔案,按需載入的話,是不是就不需要在一開始進入畫面時,一次載入一大包 JavaScript 了?

next/dynamic 就是 Next.js 提供來處理這個情境的方式。

為什麼需要使用 next/dynamic

前面的情境已經看到痛點,也就是「不是所有功能都需要在首屏就下載與執行,但是卻會影響到需載入的 JavaScript 檔案大小」。而 next/dynamic 存在的目的也就是把 「首屏非必要」的元件拆成獨立的 chunk,並且延後載入,已達成更強化的 code splitting

雖然 Next.js 在 build 時會自動做以路由為單位的拆分(每個頁面有自己的 bundle,另含共用的 chunks),但實務上只以路由為單位,在很多時候顆粒度還是不夠小。這時就可以使用 next/dynamic 把顆粒度降到「元件級」,讓大型或只在互動後才需要的元件,在要用到時才下載,從而降低首屏 JavaScript 的大小、縮短可互動時間(TTI)。

next/dynamic 使用方式

接著來看一下 next/dynamic 的使用方式。

我們延續最前面提到的那個例子,來看要怎麼透過 next/dynamic 來達到 code splitting 的效果。
原本的 ChartCard 元件的內容是以下這樣。

"use client";

import { useState } from "react";
import Chart from "./Chart";

const ChartCard = ({ title }: { title: string }) => {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <div className="flex flex-col">
      <p className="text-3xl bold">{title}</p>
      <div className="flex flex-col mt-4">
        <p>some information.....</p>
        <button
          className="mt-3 rounded-lg px-4 py-2 bg-black text-white hover:bg-black/80 transition"
          onClick={() => setIsOpen(!isOpen)}
          aria-expanded={isOpen}
        >
          {isOpen ? "Hide Chart" : "Show Chart"}
        </button>
      </div>
      <div
        className={`mt-6 w-full flex justify-center overflow-hidden transition-all duration-500 ease-out ${
          isOpen
            ? "opacity-100 scale-y-100 max-h-[1200px]"
            : "opacity-0 scale-y-95 max-h-0"
        }`}
        style={{ transformOrigin: "top" }}
      >
        {isOpen && (
          <div className="rounded-xl p-2 shadow-lg">
            <Chart />
          </div>
        )}
      </div>
    </div>
  );
};

export default ChartCard;

想要使用 next/dynamic 的話,可以把引入 Chart 的部分,改成 dynamic 的寫法。

"use client";

import { useState } from "react";
// import dynamic 
import dynamic from "next/dynamic";

// 不直接引入 Chart 元件,改用 dynamic 引入 Chart 元件
const Chart = dynamic(
  () => import("./Chart")
);

const ChartCard = ({ title }: { title: string }) => {
  const [isOpen, setIsOpen] = useState(false);
  // 略 ...
};

export default ChartCard;

實際看改寫後的結果!
https://ithelp.ithome.com.tw/upload/images/20250928/20130914dajTw3Y4wM.png

調整過後可以看到,首次進入頁面需要加載的 JavaScript 真的有大幅減少,從 262KB 減少至 103KB

不過除了這個基本的用法外,還可以在使用 next/dynamic 的時候,加上 { ssr: false } 這個設定。

如果有一個元件不僅是不需要再一進入頁面的時候就載入,而且這個元件裡面還有使用到一些只有在 client 端才可以使用的內容,例如 window、document 等瀏覽器專屬的 API,或者是一個使用檔案較大的第三方套件的元件,就可以加上 { ssr: false },用來跳過在伺服器端就先產生這個元件的內容。但是設定 { ssr: false } 的時候,因為在伺服器端上進行畫面渲染時,只會將對應區塊標示這裡會插入一個內容,例如以下這個截圖,實際上的內容會在瀏覽器上才渲染出來,
https://ithelp.ithome.com.tw/upload/images/20250928/20130914DC9GTqRn6O.png
所以如果是需要立即顯示,不是進行操作才會出現在畫面上的內容,會建議加上 loading 來佔位,以避免畫面突然變高,造成版面有位移的狀況(也就 CLS 的狀況)。

我們先把畫面都改成預設 Chart 都打開顯示,並加上 { ssr: false },先不加上 loading。

const Chart = dynamic(() => import("./Chart"), {
  ssr: false,
});

https://i.imgur.com/s4ANeG2.gif
可以看到,在畫面中,有一段時間 Chart 沒有顯示出來,因為在伺服器上沒有先處理 Chart 這部分的話,等到在瀏覽器上,Chart 載入完成後,才顯示在畫面上,造成畫面有位移的狀況。

但是當我們把 loading 加上後,就可以佔一個位置。

const Chart = dynamic(() => import("./Chart"), {
  ssr: false,
  loading: () => <div className="h-[400px] w-[600px] rounded bg-gray-100" />,
});

https://i.imgur.com/lCd5Aa4.gif
最後呈現的狀況就是在 Chart 還沒顯示出來之前,先顯示一個灰底的區塊,把 Chart 的位置佔住,等到 Chart 顯示出來,就不會有位移的狀況出現了。

next/dynamic vs React.lazy

看到這裡應該有不少人腦海中會浮現另一個很類似的用法,那就是 React.lazy。其實 React.lazy 的使用目的和 next/dynamic 幾乎是一樣的,它們都是為了進行 code splitting,以及將不需要馬上就載入的內容延遲載入的方式。在使用上,由於 next/dynamic 不需要額外使用 Suspense,所以 next/dynamic 又被視為是 React.lazy 和 Suspense 的複合用法。其中比較不一樣的地方是 next/dynamic 增加了可以略過 SSR 模式渲染的設定,React.lazy 則沒有支援這個用法。

總結

當專案逐漸龐大時,JavaScript 的載入大小會直接影響頁面首屏的速度與使用者體驗。雖然 Next.js 會以路由為單位自動做 code splitting,但這樣的顆粒度還是不夠小,這時候就可以使用 next/dynamic 來以元件為單位進行 code splitting。

使用 next/dynamic 時,能夠將非首屏必要的功能延後載入,降低首屏 JavaScript 的大小,加快進入頁面的速度。透過 { ssr: false },還能針對只在瀏覽器端可執行的元件(例如使用 window、document 或是大型第三方套件)完全跳過 SSR,並搭配 loading 屬性避免畫面位移(CLS)。與 React.lazy 相比,next/dynamic 不僅省略了額外的 Suspense 包裝,還提供略過 SSR 的能力,更適合在 Next.js 專案中使用。

next/dynamic 是 Next.js 優化 JavaScript 載入的重要工具,能讓開發者有效降低首屏負擔,並改善整體的載入體驗。


今天優化 JavaScript 的方法 next/dynamic 就看到這裡告一個段落。透過 next/dynamic 進行 code Splitting 與效能優化後,不僅能改善使用者體驗,也能因為效能指標的提升而間接對 SEO 帶來幫助。
不過想要把 SEO 做好,並不是把所有直接間接有幫助的方法都使用上去就有最佳效果,關於這部分的細節,就讓我們明天再接著好好了解一下吧,我們明天見~

參考資料

官方文件 - Lazy Loading


上一篇
【Day 25】Next.js 的圖片優化方式 - Image:你可能不需要額外使用它
系列文
從 React 學 Next.js:不只要會用,還要真的懂26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言