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

Next.js 和 Gatsby 採用較新的 webpack 分割策略,可減少重複程式碼,提升網頁載入效能。

Chrome 會與 JavaScript 開放原始碼生態系統中的工具和架構合作。我們最近新增了許多新最佳化功能,以改善 Next.jsGatsby 的載入效能。本文將說明經過改善的精細區塊化策略,目前這項策略已預設提供給這兩個架構。

簡介

與許多網頁架構一樣,Next.js 和 Gatsby 都使用 webpack 做為核心套件組合器。webpack 3.0 推出 CommonsChunkPlugin,讓您可以在單一 (或少數)「共用」區塊 (或區塊) 中,針對不同的進入點輸出共用的模組。共用程式碼可單獨下載,並提早儲存在瀏覽器快取中,進而提升載入效能。

許多單頁應用程式架構採用的進入點和套件組態,其模式如下所示:

常見的進入點和組合設定

雖然這項做法實用,但將所有共用模組程式碼綁定至單一區塊的概念有其限制。未在每個進入點共用的模組,可能會為不使用該模組的路徑下載,導致下載的程式碼比實際需要的多。舉例來說,當 page1 載入 common 區塊時,即使 page1 未使用 moduleC,它仍會載入 moduleC 的程式碼。因此,除了其他幾個原因外,webpack 4 也移除了這個外掛程式,改用新的外掛程式: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 個),因此減少 Bundler 輸出的區塊數量,可確保要求總數低於此門檻。不過,這項規則僅適用於 HTTP/1.1。在 HTTP/2 中使用多工處理功能,可透過單一來源使用單一連線,並同時傳送多個要求。換句話說,我們通常不必擔心限制 Bundler 傳送的區塊數量。

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

網頁載入效能,要求次數增加

在單一網頁上平均執行三次多項測試時,當變更最大初始要求數量 (從 5 到 15),load開始算繪首次顯示內容所需時間的時間都維持不變。有趣的是,我們發現只有在將要求分割為數百個時,才會出現輕微的效能開銷。

網頁載入效能 (有數百個要求)

這項測試顯示,只要維持在可靠的門檻 (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 設定中,這項設定預設會在 v2.20.7 中提供。

結論

提交精細區塊的概念並非 Next.js、Gatsby 或 webpack 專屬。無論使用哪種架構或模組套件編譯器,如果應用程式採用大型「公共」套件方法,則應考慮改善應用程式的分割策略。

  • 如果您想瞭解如何將相同的區塊化最佳化方式套用至一般 React 應用程式,請查看這個 React 應用程式範例。這個範例採用簡化的細目區塊化策略,可協助您開始將相同類型的邏輯套用至網站。
  • 對於匯總,系統預設會逐一建立區塊。如要手動設定行為,請參閱 manualChunks