通过精细分块提升 Next.js 和 Gatsby 网页加载性能

Next.js 和 Gatsby 中更新的 webpack 分块策略可最大限度地减少重复代码,从而改善网页加载性能。

Chrome 正在与 JavaScript 开源生态系统中的工具和框架协作。我们最近添加了一些新优化,以提升 Next.jsGatsby 的加载性能。本文介绍了经过改进的精细分块策略,目前这两个框架中均默认提供该策略。

简介

与许多 Web 框架一样,Next.js 和 Gatsby 使用 webpack 作为其核心捆绑器。webpack v3 引入了 CommonsChunkPlugin,以便在单个(或多个)“公共”分块中输出不同入口点之间共享的模块。共享代码可以单独下载,并尽早存储在浏览器缓存中,从而实现更好的加载性能。

许多单页应用框架采用如下所示的入口点和软件包配置,这种模式越来越受欢迎:

通用入口点和软件包配置

虽然将所有共享模块代码捆绑到单个分块中的概念很实用,但也存在局限性。系统可能会为未使用这些模块的路由下载在每个入口点中都未共享的模块,导致下载的代码超出必要数量。例如,当 page1 加载 common 分块时,它会加载 moduleC 的代码,即使 page1 不使用 moduleC 也是如此。因此,与一些其他插件一起,webpack v4 移除了该插件,取而代之的是一个新插件:SplitChunksPlugin

改进的分块

SplitChunksPlugin 的默认设置适用于大多数用户。系统会根据多个条件创建多个拆分区块,以防止跨多个路由提取重复的代码。

不过,许多使用此插件的 Web 框架仍采用“单个公共代码库”方法进行分块拆分。例如,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 中的多路复用允许使用单个连接通过单个源并行流式传输多个请求。换句话说,我们通常无需担心限制捆绑器发出的分块数量。

所有主流浏览器都支持 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 使用服务器端 build 清单文件来确定不同的入口点使用哪些输出分块。为了也向客户端提供此信息,我们创建了一个精简版客户端 build 清单文件,用于映射每个入口点的所有依赖项。

// 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