Next.js 和 Gatsby 中采用了一种新的 webpack 分块策略,可最大限度地减少重复代码,从而提高网页加载性能。
Chrome 正在与 JavaScript 开源生态系统中的工具和框架开展合作。最近添加了许多新的优化,以提高 Next.js 和 Gatsby 的加载性能。本文介绍了现在默认在这两个框架中提供的改进的精细分块策略。
简介
与许多 Web 框架一样,Next.js 和 Gatsby 使用 webpack 作为其核心打包器。webpack v3 引入了 CommonsChunkPlugin,以便在单个(或几个)“commons”分块中输出不同入口点之间共享的模块。共享代码可以单独下载并尽早存储在浏览器缓存中,从而提高加载性能。
随着许多单页应用框架采用如下所示的入口点和软件包配置,这种模式开始流行起来:

虽然将所有共享模块代码捆绑到单个分块中的概念很实用,但也有其局限性。未在每个入口点共享的模块可以针对不使用该模块的路由进行下载,从而导致下载的代码超出必要范围。例如,当 page1 加载 common 分块时,它会加载 moduleC 的代码,即使 page1 不使用 moduleC 也是如此。
出于此原因以及其他一些原因,webpack v4 移除了该插件,取而代之的是一个新插件:SplitChunksPlugin。
改进了分块
SplitChunksPlugin 的默认设置适用于大多数用户。系统会根据多种条件创建多个拆分块,以防止跨多个路由提取重复的代码。
不过,许多使用此插件的 Web 框架仍会遵循“单通用”方法进行分块。例如,Next.js 会生成一个 commons 软件包,其中包含在超过 50% 的网页中使用的任何模块以及所有框架依赖项(react、react-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)都会拆分为各自的单独分块
- 系统会为框架依赖项(
react、react-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、开始渲染和 First Contentful Paint 时间都保持大致相同。有趣的是,我们注意到,只有在激进地拆分为数百个请求后,才会出现轻微的性能开销。

这表明,将请求数量控制在可靠的阈值(20~25 个请求)内,可以在加载性能和缓存效率之间取得适当的平衡。经过一些基准测试后,选择 25 作为 maxInitialRequest 计数。
修改并行请求数上限后,系统会生成多个共享 bundle,并针对每个入口点对它们进行适当的分离,从而显著减少同一页面中不必要的代码量。

此实验仅涉及修改请求数量,以查看是否会对网页加载性能产生任何负面影响。结果表明,在测试页面上将 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 中通过标志推出,并在一些尝鲜者中进行了测试。许多网站的 JavaScript 总用量都大幅减少:
| 网站 | JS 变化总量 | 差异百分比 |
|---|---|---|
| https://www.barnebys.com/ | -238 KB | -23% |
| https://sumup.com/ | -220 KB | -30% |
| https://www.hashicorp.com/ | -11 MB | -71% |
最终版本已在版本 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% |
请查看 PR,了解他们如何将此逻辑实现到其 webpack 配置中,该配置在 v2.20.7 中默认提供。
总结
提供精细分块的概念并非 Next.js、Gatsby 甚至 webpack 所独有。如果应用采用大型“通用”软件包方法,则无论使用何种框架或模块打包器,都应考虑改进其应用的分块策略。
- 如果您想查看应用于普通 React 应用的相同分块优化,请查看此示例 React 应用。它使用精细分块策略的简化版本,可以帮助您开始将相同类型的逻辑应用于您的网站。
- 对于 Rollup,默认情况下会精细地创建分块。如果您想手动配置该行为,请参阅
manualChunks。