Next.js 和 Gatsby 中更新的 webpack 代码块划分策略可最大限度地减少重复代码,从而提高网页加载性能。
Chrome 正在与 JavaScript 开源生态系统中的工具和框架开展协作。最近添加了许多新的优化,以提升 Next.js 和 Gatsby 的加载性能。本文介绍了一种改进的精细分块策略,该策略现在已默认随这两个框架一起提供。
简介
与许多 Web 框架一样,Next.js 和 Gatsby 使用 webpack 作为其核心捆绑器。webpack v3 引入了 CommonsChunkPlugin
,以便能够在一个(或几个)“commons”块中输出不同入口点之间共享的模块。共享代码可以单独下载并提前存储在浏览器缓存中,从而提高加载性能。
许多单页应用框架都采用了如下所示的入口点和软件包配置,这种模式也因此而流行起来:
虽然将所有共享模块代码捆绑到一个块中的做法很实用,但也有其局限性。未在每个入口点中共享的模块可以针对不使用它的路由进行下载,从而导致下载的代码多于所需代码。例如,当 page1
加载 common
代码块时,即使 page1
不使用 moduleC
,也会加载 moduleC
的代码。出于此原因以及其他一些原因,webpack v4 移除了该插件,转而使用新插件:SplitChunksPlugin
。
改进了分块
SplitChunksPlugin
的默认设置适用于大多数用户。系统会根据多种条件创建多个拆分块,以防止跨多个路由提取重复的代码。
不过,许多使用此插件的 Web 框架仍然采用“单一通用”方法进行 chunk 拆分。例如,Next.js 会生成一个 commons
bundle,其中包含在超过 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 个),因此尽量减少打包器输出的 chunk 数量可以确保请求总数保持在此阈值以下。不过,这仅适用于 HTTP/1.1。HTTP/2 中的多路复用功能允许使用单个来源的单个连接并行传输多个请求。换句话说,我们通常不需要担心限制打包器发出的 chunk 数量。
所有主流浏览器均支持 HTTP/2。Chrome 和 Next.js 团队想知道,通过将 Next.js 的单个“commons”软件包拆分为多个共享 chunk 来增加请求数量,是否会以任何方式影响加载性能。他们首先测量单个网站的性能,同时使用 maxInitialRequests
属性修改并行请求数上限。
在单个网页上多次运行多次试验后,平均而言,当初始请求数上限从 5 变为 15 时,load
、start-render 和首次内容渲染时间都保持大致不变。有趣的是,我们注意到只有在积极拆分到数百个请求后,才会出现轻微的性能开销。
这表明,保持在可靠的阈值(20~25 个请求)以下可在加载性能和缓存效率之间取得适当的平衡。经过一些基准测试后,我们选择 25 作为 maxInitialRequest
数值。
修改并行发生的请求数量上限后,我们得到了多个共享 bundle,并针对每个入口点对它们进行了适当分离,从而显著减少了同一页面上不必要的代码量。
此实验仅涉及修改请求数量,以查看是否会对网页加载性能产生任何负面影响。结果表明,将测试网页上的 maxInitialRequests
设置为 25
是最佳选择,因为这样可以减小 JavaScript 载荷大小,而不会减慢网页速度。水合网页所需的 JavaScript 总量仍然大致相同,这说明了为什么网页加载性能不一定会随着代码量的减少而提高。
webpack 使用 30 KB 作为要生成的块的默认最小大小。不过,将 maxInitialRequests
值设为 25 并将最小大小设为 20 KB 反而实现了更好的缓存效果。
通过精细分块实现大小缩减
许多框架(包括 Next.js)都依赖客户端路由(由 JavaScript 处理)来为每次路由转换注入新的脚本标记。但它们如何在 build 时预先确定这些动态块?
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 用量都大幅减少:
网站 | 总 JavaScript 更改 | 差异百分比 |
---|---|---|
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 大幅减少:
网站 | 总 JavaScript 更改 | 差异百分比 |
---|---|---|
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 应用。它使用了精细分块策略的简化版本,可帮助您开始将相同的逻辑应用于您的网站。
- 对于汇总,默认情况下会以精细方式创建 chunk。如果您想手动配置此行为,请参阅
manualChunks
。