通过精细分块提升 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的默认设置适用于大多数用户。系统会根据多个conditions创建多个拆分块,以防止跨多个路由提取重复代码。

但是,许多使用此插件的 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

这种精细的分块策略具有以下优势:

  • 缩短了网页加载时间。发出多个共享区块而不是单个区块,可以最大限度地减少任何入口点的不需要(或重复)代码量。
  • 改进了导航期间的缓存。将大型库和框架依赖项拆分为单独的区块可降低缓存失效的可能性,因为两者在升级之前不太可能更改。

您可以查看 Next.js 在 webpack-config.ts 中采用的完整配置。

更多 HTTP 请求

SplitChunksPlugin 定义了精细分块的基础,而将此方法应用于 Next.js 这样的框架并不是一个全新的概念。但是,由于一些原因,许多框架仍然继续使用单一启发式和“通用”捆绑包策略。这包括担心更多 HTTP 请求可能会对网站性能产生负面影响。

浏览器只能打开与单个源站的有限数量的 TCP 连接(Chrome 为 6 个),因此,尽量减少打包器输出的分块数量可确保请求总数低于此阈值。不过,这仅适用于 HTTP/1.1。HTTP/2 中的多路复用支持在单个来源上使用单个连接并行流式传输多个请求。换句话说,我们通常不需要关注捆绑器发出的区块数量。

所有主流浏览器均支持 HTTP/2。Chrome 和 Next.js 团队想知道,通过将 Next.js 的单个“commons”内容包拆分为多个共享区块来增加请求数量,是否会对加载性能产生任何影响。他们首先测量单个网站的性能,同时使用 maxInitialRequests 属性修改最大并行请求数量。

随着请求数量增加,网页加载效果有所提升

在一个网页上平均运行三次多次试验后,当初始请求数量上限不同(从 5 到 15)不同时,loadstart-renderFirst Contentful Paint 时间会大致保持不变。有趣的是,只有在积极拆分为数百个请求后,我们才发现性能开销略有下降。

数百个请求的网页加载性能

这表明,低于可靠的阈值(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 过去的做法与使用基于使用情况的启发法定义通用模块的方法相同:

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,了解他们如何将此逻辑实现到 v2.20.7 中默认提供的 webpack 配置中。

总结

提供精细数据块的概念并非特定于 Next.js、Gatsby,甚至是 webpack。无论使用何种框架或模块打包器,如果应用遵循大型“通用”软件包方法,则每个人都应该考虑改进其应用的分块策略。

  • 如果您希望查看在原版 React 应用中应用相同的分块优化,请参阅此 React 应用示例。该应用使用简化版本的精细分块策略,可帮助您开始对网站应用同种逻辑。
  • 对于 Rollup,系统默认创建区块。如果您想手动配置行为,请查看 manualChunks