通过精细分块提升 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 的单个“公共”软件包拆分为多个共享分块来增加请求数量,是否会以任何方式影响加载性能。他们首先测量了单个网站的性能,同时使用 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