在 Next.js 16.3 中调试 Turbopack 的新分块控制功能
深入探讨 Next.js 16.3 中新增的 turbopackChunking 配置,说明 maxChunkCountPerGroup 与 generateComponentChunks 如何影响代码包大小及缓存效果。
那是周五的4点40分,正是人们准备关上笔记本电脑开始周末休息的时刻,一个用于仪表板应用的常规部署却突然不再那么“常规”了。原本40秒就能完成的构建过程这次耗时超过了3分钟,主路由的客户端打包文件大小也增加了近300kb。没有任何路由被修改,也没有引入任何可能解释这一现象的新依赖项。那周唯一的变动就是Next.js版本的小幅升级。接下来的一个晚上,我只能盯着“网络”标签页,看着四个庞大的JavaScript文件依次加载,而以前只需要十几个较小的文件就能完成同样的工作。
问题并非出在漏洞上,而是Turbopack的代码分块逻辑完全按照设计运行,只是结果并非人们所期望的那样。
为何代码分块问题始终未能真正解决
这其实是打包工具大约十年来一直在尝试解决的问题。Webpack的SplitChunksPlugin提供了如minSize、maxSize和cacheGroups之类的配置选项,但实际上几乎没人真正从原理上理解过它们,这些配置大多是从2019年某个Stack Overflow帖子中照搬来的。其中存在的权衡始终存在:将代码合并为更少但体积更大的块可以减少HTTP请求次数,但却会损害缓存效果,因为一次CSS文件的更改就可能导致500kb大小的代码包失效;而将其拆分为许多较小的块虽然能提升缓存的精细度,却会增加数据传输的次数,在网络速度较慢的情况下这种影响会很快显现出来。
Turbopack也延续了这种平衡策略,直到最近仍倾向于强制进行代码合并。对于单页应用而言,这是个合理的选择,因为大多数资源本来就只会被加载一次。不过,如果用户需要在大型应用中反复导航,这种做法就会带来问题——因为之前缓存的代码会不断被重新下载,与新生成的代码一起打包传输。
16.3版本有哪些变化
在Next.js 16.3中,代码合并的行为不再隐藏在某个复杂的内部机制中,而是通过配置直接体现出来。新增了turbopackChunking配置块,其中最重要的两个设置分别是maxChunkCountPerGroup和maxMergeChunkSize。
// next.config.js
module.exports = {
experimental: {
turbopackChunking: {
maxChunkCountPerGroup: 40, // default
maxMergeChunkSize: 200_000, // bytes, default
},
},
};
将maxChunkCountPerGroup设置得越低,Turbopack就会采用更激进的合并策略:每次页面加载时发出的请求越少,但在不同导航之间的缓存保留能力越弱。提高该值则会生成更小且数量更多的代码块,即便应用程序中无关的部分发生变化,这些代码块依然保持完整。maxMergeChunkSize则限定了合并后的代码块在多大尺寸之前,Turbopack仍会继续将其与其他代码合并在一起,这对于那些体积庞大的第三方依赖项容易占据周围所有资源的情况非常有用。
此前并不存在这些调整选项,用户只能接受默认策略的结果并设法应对。
组件分块选项
另一个值得注意的新增功能是generateComponentChunks,它将Turbopack的分块机制扩展到单个组件层面,而不再仅限于路由级别的边界。
module.exports = {
experimental: {
turbopackChunking: {
generateComponentChunks: true,
},
},
};
启用此功能后,Turbopack会同时输出已合并与未合并的代码块版本,让运行时在加载时根据浏览器已缓存的资源决定获取哪个版本。如果某个共享组件是从之前的页面访问中缓存下来的,运行时就可以请求较小的未合并代码块,而无需加载那些会重复已有代码的已合并包。这与试图提前预计算理想代码块大小的做法有本质区别,这是首个打包工具将此类决策推迟到请求时再做的案例,而非在构建时就固定下来。
我们的实际观察结果
有一支团队在仪表板应用上测试了这一功能,他们开启了generateComponentChunks选项,随后观察了几天的变化情况。初始页面加载时间几乎没有变化,仅减少了大约2%。但对于像仪表板这样的应用而言,重复导航才是实际使用中的主要场景,此时每次导航所传输的JS文件大小平均减少了约35%。这正是该功能的核心价值所在:它并非旨在缩短首次加载时间,而是针对用户连续十分钟不断在应用中切换页面的情况而设计的。目前网上流传的大多数代码包大小测试案例所测量的都是完全不同的场景,无法反映那些具有大量内部导航功能的应用在实际使用中的情况。
同一团队还重新调整了位于同一个单仓库中的某个营销网站的maxChunkCountPerGroup值,将其从40下调至24。该网站的主要访问形式为单页会话,因此更细粒度的分块所导致的额外网络请求纯粹是浪费资源,并无实际益处。虽然使用的工具相同,但调整方式却相反,而这两种选择在各自的应用场景下都是正确的。这或许才是真正的启示:并不存在一种适用于所有情况的理想分块策略,Turbopack只是自动为你做出这个选择而已。
这与旧版Webpack的配置选项有何不同
对于那些多年来一直在研究 splitChunks.cacheGroups 的人来说,新系统中最接近的对应项是 maxMergeChunkSize。与 Webpack 的许多命名习惯不同,这个参数的实际表现完全符合其名称所暗示的功能。Webpack 的配置选项多达数十个,且文档描述十分简略,这些选项之间的相互作用方式也无人能完全理清,许多团队最终只能沿用已离职工程师留下的配置文件,因为大家都不敢擅自修改。Turbopack 的设计则更为精简——它只提供两三个控制选项,虽然因此牺牲了处理极少数特殊情况的灵活性,但大大缩短了从“发现打包结果异常”到确定具体需要调整哪个数值的时间。这种权衡是值得接受的,尤其是对于那些不想再陷入复杂配置困境的人而言。
还有另一个值得单独提及的区别。Webpack 在很大程度上迫使开发者在构建时就要考虑代码分块的问题,通过查看 stats.json 的输出来尝试预测真实访问者的浏览器缓存中会存储什么内容。而 Turbopack 的组件级分块则将这部分决策工作转移到了请求阶段,运行时可以直接检查实际的缓存状态,而无需提前猜测。这其实是一种真正的架构变革,而不仅仅是配置标志的更名而已。
缺点
该功能仍被标记为实验性版本,相关文档也明确说明了这一点。有一个团队在部署过程中遇到过这样的情况:动态导入的图表库被分割后,导致生产环境中短暂出现“无法加载代码块”的错误,这正是那种几乎影响所有支持代码分割的打包工具的老问题——过时的代码块引用问题。对于遇到该问题的用户来说,强制刷新可以解决故障,但这也提醒我们,更细粒度的代码块分割实际上会扩大这种故障发生的概率,而非降低它。
在没有真实流量数据的情况下调整这些设置其实纯属猜测。在获得实际的导航分析数据之前,最好先不要擅自修改新项目的默认设置,这些数据能显示访问者是只浏览一次就离开,还是会在单次会话中切换十几种不同页面。如果还没有这类数据,那这就是在调整任何分块配置之前开始收集数据的绝佳理由。盲目调整很可能会导致缓存命中率比默认设置还要低,而这种性能下降往往不会被察觉。没人会提交标题为“导航速度似乎慢了些”的错误报告,他们只会直接不再回来。
那么,值得开启吗?
在控制面板场景中,答案是肯定的,相关设置保持启用状态。而对于营销网站,则只需调整一个数值即可,仅此而已。这些措施都无法修复本质上存在缺陷的应用程序;如果由于使用了不必要的依赖项而导致包体过大,再好的分块策略也无法弥补这一错误。该功能所提供的只是针对特定问题的精准解决方案,这与今年推出的许多构建工具功能形成了鲜明对比——对于那些曾花费大量时间处理这类漏洞的人来说,这一评价确实很有说服力。