透過精細的分塊改善 Next.js 和 Gatsby 網頁載入效能

Next.js 和 Gatsby 中較新的 webpack 區塊策略可減少重複的程式碼,進而提升網頁載入效能。

Houssein Djirdeh
Houssein Djirdeh

Chrome 正在與 JavaScript 開放原始碼生態系統中的工具和架構合作。我們最近新增了多項最佳化功能,可提升 Next.jsGatsby 的載入效能。本文將介紹改良的細部分塊策略,現在這兩個架構預設都會提供這項策略。

簡介

與許多網頁架構一樣,Next.js 和 Gatsby 都使用 webpack 做為核心套件組合工具。webpack v3 導入了 CommonsChunkPlugin,可輸出在單一 (或少數)「通用」區塊 (或區塊) 中,不同進入點之間共用的模組。共用程式碼可以分開下載,並預先儲存在瀏覽器快取中,進而提升載入效能。

許多單頁應用程式架構都採用了這個模式,並採用類似下列的進入點和套件設定:

常見的進入點和套件組合設定

雖然將所有共用模組程式碼打包成單一區塊的概念很實用,但也有其限制。如果模組未在每個進入點共用,系統可能會為未使用該模組的路徑下載模組,導致下載的程式碼超出必要範圍。舉例來說,當 page1 載入 common 區塊時,即使 page1 未使用 moduleC,也會載入 moduleC 的程式碼。因此,webpack v4 移除了這個外掛程式,改用新的外掛程式:SplitChunksPlugin

改良式分塊

SplitChunksPlugin 的預設設定適用於大多數使用者。系統會根據多項條件建立多個分割區塊,避免在多條路徑中擷取重複的程式碼。

不過,許多使用這個外掛程式的網頁架構,仍採用「單一通用」方法來分割區塊。舉例來說,Next.js 會產生 commons 組合,其中包含超過 50% 網頁使用的任何模組,以及所有架構依附元件 (reactreact-dom 等)。

const splitChunksConfigs = {
  
  prod: {
    chunks: 'all',
    cacheGroups: {
      default: false,
      vendors: false,
      commons: {
        name: 'commons',
        chunks: 'all',
        minChunks: totalPages > 2 ? totalPages * 0.5 : 2,
      },
      react: {
        name: 'commons',
        chunks: 'all',
        test: /[\\/]node_modules[\\/](react|react-dom|scheduler|use-subscription)[\\/]/,
      },
    },
  },

雖然將架構相關程式碼納入共用區塊,代表任何進入點都可以下載並快取該程式碼,但根據使用情況,納入超過一半頁面使用的常見模組,並非非常有效。修改這項比例只會導致下列兩種結果之一:

  • 如果降低比率,系統會下載更多不必要的程式碼。
  • 如果提高比率,多個路徑就會有更多重複的程式碼。

為解決這個問題,Next.js 採用了不同的設定SplitChunksPlugin可減少任何路徑的不必要程式碼。

  • 任何足夠大的第三方模組 (大於 160 KB) 都會分割成自己的個別區塊
  • 系統會為架構依附元件 (reactreact-dom 等) 建立個別的 frameworks 區塊
  • 視需要建立多個共用區塊 (最多 25 個)
  • 生成區塊的最小大小變更為 20 KB

這種細微的切塊策略有下列優點:

  • 網頁載入時間縮短。發出多個共用區塊 (而非單一區塊),可盡量減少任何進入點的不必要 (或重複) 程式碼量。
  • 改善導覽期間的快取功能。將大型程式庫和架構依附元件分割成個別區塊,可降低快取失效的可能性,因為這兩者在升級前不太可能變更。

您可以在 webpack-config.ts 中查看 Next.js 採用的完整設定。

更多 HTTP 要求

SplitChunksPlugin 定義了細部分塊的基礎,而將這種做法套用至 Next.js 等架構並非全新概念。不過,許多架構仍繼續使用單一啟發式方法和「通用」套件策略,原因如下:包括擔心 HTTP 要求過多會對網站效能造成負面影響。

瀏覽器只能對單一來源開啟有限數量的 TCP 連線 (Chrome 為 6 個),因此盡量減少打包工具輸出的區塊數量,可確保要求總數維持在這個門檻以下。不過,這只適用於 HTTP/1.1。HTTP/2 中的多工處理功能可透過單一來源的單一連線,並行串流多個要求。換句話說,我們通常不必擔心限制 bundler 發出的區塊數量。

所有主要瀏覽器都支援 HTTP/2。Chrome 和 Next.js 團隊想瞭解,如果將 Next.js 的單一「commons」套件分割成多個共用區塊,增加要求數量是否會影響載入效能。他們先測量單一網站的效能,同時使用 maxInitialRequests 屬性修改並行要求的數量上限。

要求數量增加時的網頁載入效能

在單一網頁上平均執行三次多項測試後,發現當最大初始要求計數從 5 變更為 15 時,loadstart-render首次顯示內容所需時間都維持在相同時間。有趣的是,我們發現只有在積極將要求分割成數百個時,才會出現輕微的效能負擔。

數百個請求的網頁載入效能

這表示維持在可靠的門檻 (20 到 25 個要求) 以下,可在載入效能和快取效率之間取得適當平衡。經過一些基準測試後,我們選取 25 做為 maxInitialRequest 數量。

修改平行發生的要求數量上限後,產生了不只一個共用套件,並為每個進入點適當區隔,大幅減少同一網頁中不必要的程式碼量。

增加區塊數量,減少 JavaScript 酬載

這項實驗的目的只是修改要求數量,看看是否會對網頁載入效能造成負面影響。結果顯示,將測試網頁的 maxInitialRequests 設為 25 是最佳做法,因為這樣可縮減 JavaScript 酬載大小,同時不會降低網頁速度。網頁水合作用所需的 JavaScript 總量仍大致相同,這說明瞭為什麼程式碼量減少後,網頁載入效能不一定會提升。

webpack 會將 30 KB 設為預設的最低區塊生成大小。不過,將 maxInitialRequests 值設為 25,並將最小大小設為 20 KB,反而能獲得較佳的快取效果。

透過精細區塊縮減大小

許多架構 (包括 Next.js) 都會依賴用戶端路徑 (由 JavaScript 處理),在每次路徑轉換時插入新的指令碼標記。但他們如何在建構時預先決定這些動態區塊?

Next.js 會使用伺服器端建構資訊清單檔案,判斷不同進入點使用的輸出區塊。為將這項資訊提供給用戶端,我們也建立了簡短的用戶端建構資訊清單檔案,以便對應每個進入點的所有依附元件。

// Returns a promise for the dependencies for a particular route
getDependencies (route) {
  return this.promisedBuildManifest.then(
    man => (man[route] && man[route].map(url => `/_next/${url}`)) || []
  )
}
Next.js 應用程式中多個共用區塊的輸出內容。

這項較新的細微區塊策略最初是在 Next.js 中推出,並以標記的形式提供,供早期採用者測試。許多網站的 JavaScript 總用量大幅減少:

網站 JS 總變化 差異百分比
https://www.barnebys.com/ -238 KB -23%
https://sumup.com/ -220 KB -30%
https://www.hashicorp.com/ -11 MB -71%
減少所有路徑的 JavaScript 大小 (壓縮後)

最終版本預設會隨附於9.2 版

Gatsby

Gatsby 過去也採用相同方法,使用以用量為準的啟發式演算法定義常見模組:

config.optimization = {
  
  splitChunks: {
    name: false,
    chunks: `all`,
    cacheGroups: {
      default: false,
      vendors: false,
      commons: {
        name: `commons`,
        chunks: `all`,
        // if a chunk is used more than half the components count,
        // we can assume it's pretty global
        minChunks: componentsCount > 2 ? componentsCount * 0.5 : 2,
      },
      react: {
        name: `commons`,
        chunks: `all`,
        test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
      },

他們也透過最佳化 webpack 設定,採用類似的細微區塊策略,發現許多大型網站的 JavaScript 大幅減少:

網站 JS 總變化 差異百分比
https://www.gatsbyjs.org/ -680 KB -22%
https://www.thirdandgrove.com/ -390 KB -25%
https://ghost.org/ -1.1 MB -35%
https://reactjs.org/ -80 Kb -8%
減少所有路徑的 JavaScript 大小 (壓縮後)

請參閱 PR,瞭解他們如何在 webpack 設定中實作這項邏輯,而這項設定預設會隨附於 2.20.7 版。

結論

細部區塊的運送概念不限於 Next.js、Gatsby,甚至 webpack。如果應用程式採用大型「commons」套件方法,無論使用哪個架構或模組套件工具,都應考慮改善應用程式的區塊策略。

  • 如要瞭解如何將相同的區塊最佳化套用至原生 React 應用程式,請參閱這個 React 應用程式範例。這個範例採用簡化的細微區塊策略,可協助您開始將相同的邏輯套用至網站。
  • 對於 Rollup,系統預設會以細微程度建立區塊。如要手動設定這項行為,請參閱 manualChunks