透過精細的分塊改善 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 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 個),因此減少 Bundler 輸出的區塊數量,可確保要求總數低於此門檻。不過,這項規則僅適用於 HTTP/1.1。透過 HTTP/2 的多工處理功能,您可以透過單一來源使用單一連線,並同時傳送多個要求。換句話說,我們通常不需要擔心組合器發出的區塊數量。

所有主要瀏覽器都支援 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 值與大小下限為 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)[\\/]/,
      },

在最佳化 Web Pack 設定以採用類似的精細分塊策略後,他們也注意到許多大型網站的 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