How Turbopack chunks your JavaScript
TL;DR · AI 摘要
Turbopack通过智能分块优化JavaScript加载性能,平衡代码复用与网络请求开销。
核心要点
- 单块策略导致代码冗余,多块策略增加请求数量,需权衡缓存效率与传输开销。
- HTTP/2降低单请求成本,但大量小文件仍影响压缩效率。
- Turbopack采用模块级分块,结合代码共享与请求合并优化实际性能。
结构提纲
按章节快速跳转。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- Turbopack分块策略
- 分块粒度
- 单块策略
- 页面级分块
- 模块级分块
- 性能挑战
- HTTP/2请求开销
- 压缩效率下降
金句 / Highlights
值得收藏与分享的关键句。
单块策略使每个页面加载1.09MB代码,但后续访问均为缓存命中。
模块级分块导致355个独立请求,HTTP/2仍无法完全消除请求开销。
gzip压缩在大量小文件场景下效率下降30%-40%。
Next.js 中 Turbopack 如何对 JavaScript 进行分块
返回博客
2026年9月3日,星期四
Turbopack 如何对 JavaScript 进行分块
作者:
Sam Poder
@sam_poder
打开这个页面的网络面板,你会看到一系列看似随机命名的 JavaScript 文件:
网络
正在收集请求...
点击其中一个文件,取消混淆后,你会看到类似以下内容:
36wnellv-yn9q.js
(
globalThis
.
TURBOPACK
||
(
globalThis
.
TURBOPACK
=
[]))
.push
([
"object"
==
typeof
document
?
document
.currentScript
:
void
0
,
7284
,
(e)
=>
{
"use strict"
;
function
r
() {
for
(
var
e
,
r
,
o
=
0
,
t
=
""
,
l
=
arguments.
length
; o
<
l; o
++
) {
// ...
}
return
t;
}
e
.s
([
"clsx"
,
0
,
r
,
"default"
,
0
,
r]);
}
,
395264
,
(e)
=>
{
"use strict"
;
var
r
=
e
.i
(
7284
);
let
o
=
function
() {
// ...
};
}
,
]);
//# debugId=e60a5ad0-3ac6-bb3a-e12b-6130e99fb7a6这就是一个块。Turbopack 会为你的 Next.js 应用生成数十个这样的块,其中包含你自己的代码、你依赖的包以及将所有内容连接在一起的运行时。
“分块”是决定哪些代码进入哪个块的过程。有许多不同的分块方式,每种方式都有其自身的权衡。
让我们从最简单的方式开始:一个块包含你应用中使用的所有 JavaScript。
所有 355 个模块都在一个块中,每个页面加载时都会加载。
1 个块 · 1.09 MB
app.js
现在这个块会在每个页面加载时被加载。由于每个页面共享同一个块,第一次加载后所有后续加载都会命中缓存。不错,导航速度会很快。
不过你可能已经注意到了权衡之处。即使某个页面几乎不使用 JavaScript,仍然需要加载所有其他页面的 JavaScript。唉。随着网站规模的增长,每次页面加载都会变得越来越重。从长远来看,这并不现实。
我们可以改为每个页面一个块。这样可以保持块的精简,我们永远不会过度传输代码。完美!
每个页面一个块。相同的 355 个模块被分割成 8 个块,页面之间共享的内容会在每个需要的块中重复。
8 个块 · 1.20 MB
/
/docs
/learn
/blog
/showcase
/team
/governance
/telemetry
但这样会失去缓存的优势。如果每个页面都使用 <Footer />,它的代码会出现在每个页面的块中。访问者打开四个页面时,会下载四次相同的页脚代码。
所以我们需要更精细的解决方案。假设每个模块都有自己的块?这样永远不会出现过度传输,页面之间共享的模块也只需下载一次。
每个模块一个块:355 个独立的块,每个块只传输一次,每个块都需要一次网络请求。
355 个块 · 1.09 MB
很好,但有一个小问题:这会带来数百次针对小 JavaScript 文件的网络请求。每个请求都带有开销,数百次请求会显著减慢网站速度。HTTP/2 虽然让请求成本降低,但每个请求仍然有开销。压缩算法(如 gzip)在处理大量小文件时表现更差,因为它们只能在一个文件内查找重复模式。压缩字典有所帮助,但无法解决这个问题。
更少的请求或更少的代码
因此我们面临的挑战是:我们希望实现尽可能小的下载体积和尽可能少的请求。不幸的是,这两个目标相互冲突。更少、更大的块意味着更少的请求,但块越大,它们在页面间的可重用性就越差。
Turbopack 的解决方案是将较小的块合并成更大的块。困难的部分在于决定哪些块需要合并,以及何时合并真正有益。
为了解决这个问题,我们需要引入一个新的概念——块组(chunk group)。块组是由一起加载的多个块组成的集合。例如,所有用于 /home 路径的块属于一个块组,而 /blog 路径的块属于另一个块组:
两个块组,每个路由对应一个块组。/home 的块组加载三个块,/blog 的块组加载两个块。其中一个块同时属于两个块组,因此在合并时只能在组内保持安全。
块组解决了过度传输问题。Turbopack 只会合并同一组内的块,而组内的块本来就会一起加载,因此合并它们不会增加页面原本未下载的内容。
但如何在不给浏览器带来过多请求负担的情况下优化缓存命中率呢?
为此,设想两种访问场景:一种是访问页面后离开,另一种是访问页面后导航到另一个页面。这种分析自然地扩展到包含三个或更多页面访问的会话中。
对于这些场景,我们来分析合并 <Footer /> 和 <VideoPlayer /> 的块的代价或收益。将 <Footer /> 的块称为 A,将 <VideoPlayer /> 的块称为 B。
A 出现在每个页面中,而 B 仅出现在首页。两者都属于首页的块组,因此是合并的候选对象。
如果访客加载首页后离开,合并总是有利的。它只需要一次请求而不是两次,而且这两个块本来就需要加载。
如果他们加载第二个页面,结果取决于该页面是否需要两个块或仅需要一个。假设他们从首页导航到法律页面。法律页面需要 A 但不需要 B。由于 A 和 B 被合并为一个文件,在法律页面上该文件变得无用,因此浏览器会单独重新下载 A。此时访客已经下载了 A 两次。
只有在导航过程中两个页面都需要两个块时,合并才有价值。此时合并后的文件会被重复使用,从而节省了一次请求。
以下是其他可能的导航场景。下表中的每一行代表一个两页面会话,写法为:第一个页面所需内容,然后是第二个页面所需内容。数字显示合并 A 和 B 后,与保持独立相比,该会话的总请求数和下载代码量的变化。
导航
请求数变化
下载代码量变化
A→
0
BA + B+
−1
在考虑合并块 A 和 B 时,我们需要计算仅使用 A 的块组数量、仅使用 B 的块组数量以及使用 A + B 的块组数量。我们根据每种场景发生的概率来加权合并的代价或收益。
最后,我们根据访问单页和访问多页的会话概率对这两种情况进行加权。我们估计 2/3 的会话只访问单页,而 1/3 的会话涉及两个或更多页面。
三种分块策略
这是我们讨论过的分块策略的对比。
为了生成这些数据,我使用三种不同的 Turbopack 分块配置访问了 nextjs.org:永不合并、默认配置、以及在每个块组内合并所有块。
在每个版本中,我运行了相同的导航序列,测量请求数和下载的客户端 JavaScript 体积。
无合并
Turbopack 默认
每组一个块
1
nextjs.org(初始页面加载)
363.6 KiB(76 个请求)
344.2 KiB(24 个请求)
315.3 KiB(6 个请求)
2
/blog35.0 KiB(4)
35.0 KiB(3)
34.0 KiB(2)
3
/blog/next-16-3-turbopack6.0 KiB(2)
8.7 KiB(1)
38.3 KiB(1)
4
/learn7.4 KiB(5)
6.6 KiB(2)
5
/learn/dashboard-app109.6 KiB(3)
115.0 KiB(3)
142.0 KiB(1)
6
/learn/dashboard-app/getting-started0 KiB(0)
7
/showcase24.6 KiB(2)
25.5 KiB(2)
25.0 KiB(1)
8
/docs15.4 KiB(4)
19.9 KiB(3)
48.8 KiB(2)
总体情况
561.6 KiB(96次请求)
554.8 KiB(38次请求)
610.0 KiB(15次请求)
如你所见,分块处理是一个需要权衡的优化过程。在nextjs.org上,使用默认配置相比不进行任何合并,可以将请求数减少一半以上,同时仅略微增加代码体积;而最大合并虽然进一步减少了请求数,但总体代码体积增加了10%。不过,如果我们访问的页面更少,最大合并反而会更优。将同一组中的所有块合并为一个大块虽然能提升首屏加载性能,但长期来看会带来额外开销。
Next.js 16.3 的新分块特性
影响分块效果的有两个主要限制因素。首先,合并决策是在构建时完成的,无法根据浏览器已有的缓存内容进行动态调整。其次,算法需要猜测用户在网站中的浏览路径,这也是为什么单页访问的权重被设为2/3的原因。今年夏天,作为Turbopack团队实习生,我参与改进了这两个方面,同时优化了初始分块内容的精简策略。
更智能的分块加载机制
上表展示了合并分块的成本。当访客先加载包含A的页面,再加载包含A+B的页面时,合并分块会导致A被下载两次。打包工具无法避免这种情况,因为构建时无法知道浏览器已有的缓存内容。但运行时可以做到。
在Next.js 16.3或更高版本中,通过在next.config.js中启用experimental.turbopackChunking.generateComponentChunks,Turbopack会同时生成合并版和未合并版的分块文件。我们跟踪每个合并分块的组成项,并记录哪些已加载。在请求时,我们可以选择更优方案:使用合并分块,或仅加载缺失的部分。
这个机制也适用于反向场景。如果访客已加载过合并版的A+B,我们可以跳过单独加载A的步骤。
这种优化使软导航加载更少的冗余代码,同时保留合并分块的优势而无需付出导航成本。
通过这个演示应用可以直观感受差异:
generateComponentChunks: false
component-chunks-demo-off.labs.vercel.dev
component-chunks-demo-off.labs.vercel.dev/?dark
generateComponentChunks: true
component-chunks-demo-on.labs.vercel.dev
component-chunks-demo-on.labs.vercel.dev/?dark
我们还在实验性支持only-if-cached指令,该特性允许我们检查访客到达时的缓存状态。这将使那些离开网站后返回的用户也能享受到相同优化效果。
基于分析的分块策略
你可能已经注意到,我们的分块算法对网站特性做了很多假设。例如2/3的权重设置只是一个估算值。虽然这个默认值适用于大多数网站,但未必是特定网站的最佳选择。如果你了解用户在你网站的实际浏览路径,可以提供这些数据进行优化。
现在你可以在next.config.js中通过experimental.turbopackChunking配置以下参数:
- firstPageLoadPriority:调整单页和双页场景的权重分配。取值范围0-1,数值越高越优先保证首屏加载速度,可能以增加导航成本为代价。跳出率是一个合理的初始参考值,我们默认使用0.67。
- priorityRoutes:指定加载速度至关重要的页面列表。在这些路由上,我们将更积极地进行分块合并。
- clusters(集群):经常一起访问的路由组,每个集群由正则表达式数组定义。在集群内部,我们会更积极地合并重叠的代码块。但如果一个集群混合了仅使用 A 或仅使用 B 的页面,以及同时使用 A 和 B 的页面,合并的力度会减弱。
更小且更少的代码块
以上内容都是关于如何分组代码的策略。接下来这一组功能则聚焦于从源头减少需要传输的代码量。我为此实现了以下特性:
- 对 CJS 模块进行树摇:此前我们仅支持 ESM 模块的树摇,因此 CJS 模块中未使用的导入和导出会传输到客户端。通过启用
experimental.turbopackCjsTreeShaking可开启该功能。未来 Next.js 的某个版本中该功能将默认启用。同时我也扩展了可分析和进行树摇的 ESM 模块范围,包括改进动态导入中对 barrel 文件的支持。
- 共享的 Turbopack 运行时:现在一个全局运行时代码块将取代每个页面的独立代码块。通过启用
experimental.turbopackSharedRuntime可开启该功能。该功能可减少一次阻塞请求,并在首次导航后每次导航节省约 10KB 的客户端 JavaScript 代码。该功能未来也将默认启用。
- 更轻量的默认运行时:运行时默认不再包含 WebAssembly 和 Web Worker 代码。我们会在检测到使用这些模块时,动态插入对应的加载代码。
立即体验
你可以在 Next.js 16.3 或更高版本中尝试这些新特性。如果你想深入了解代码块(包括 CSS 代码块)的机制,可以查看 Tobias 最近关于代码块权衡与限制的演讲。