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

Next.js 和 Gatsby 採用新的 Webpack 區塊處理策略,盡可能減少重複的程式碼,藉此改善網頁載入效能。

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

引言

如同許多網路架構,Next.js 和 Gatsby 一樣使用 webpack 做為核心組合。Webpack v3 導入了 CommonsChunkPlugin,以在單一 (或少數)「共同」區塊 (或區塊) 輸出不同進入點之間共用的模組。共用的程式碼可單獨下載並儲存在瀏覽器快取中,因此可以加快載入效能。

許多單頁應用程式架構都採用進入點和套件組合設定,因此這個模式相當受歡迎,如下所示:

常見的進入點和套件設定

雖然實際上可行,但如果您將所有共用模組程式碼封裝成單一區塊,其概念仍具有限制。針對未使用每個進入點的路徑,您可以下載未共用的模組,進而產生多餘的程式碼。舉例來說,當 page1 載入 common 區塊時,即使 page1 並未使用 moduleC,仍會載入 moduleC 的程式碼。因此,Webpack v4 與另外一些外掛程式已移除此外掛程式,並改用新的外掛程式:SplitChunksPlugin

改善分解

SplitChunksPlugin 的預設設定適用於大多數使用者。根據各種conditions建立多個分割區塊,以防止跨多個路徑擷取重複的程式碼。

然而,許多使用此外掛程式的網路架構仍會遵循「單一常見」方法進行區塊分割。舉例來說,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 的單一「常見」組合分割為多個共用區塊,藉此瞭解增加要求數量是否會對載入效能造成任何影響。他們一開始先測量單一網站的效能,再使用 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 用於遵循使用以使用量經驗法則定義常見模組的相同方法:

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。無論使用何種架構或模組組合器,只要遵循大型的「共同」套件方法,每個人都應考慮改善應用程式的區塊化策略。

  • 如果您想將相同的區塊最佳化調整套用至基本功能 React 應用程式,請參閱這個 React 應用程式範例。這個範例應用程式採用簡化版的精細區塊策略,可協助您開始為網站套用相同的邏輯。
  • 就 Rollup 來說,根據預設,區塊會建立更精細的區塊。如要手動設定行為,請查看 manualChunks