Next.js Blog

How Turbopack chunks your JavaScript

8.5内容质量

TL;DR · AI 摘要

Turbopack通过智能分块优化JavaScript加载性能,平衡代码复用与网络请求开销。

核心要点

  • 单块策略导致代码冗余,多块策略增加请求数量,需权衡缓存效率与传输开销。
  • HTTP/2降低单请求成本,但大量小文件仍影响压缩效率。
  • Turbopack采用模块级分块,结合代码共享与请求合并优化实际性能。

结构提纲

按章节快速跳转。

  1. 通过浏览器开发者工具展示Turbopack生成的随机命名JavaScript块。

  2. 所有模块打包成单个1.09MB块,实现缓存命中但造成代码冗余。

  3. 按页面分割成8个块,消除冗余但破坏缓存共享机制。

  4. 355个模块独立分块,解决代码复用但增加HTTP请求压力。

  5. HTTP/2降低单请求成本,但大量小文件影响压缩算法效率。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • Turbopack分块策略
    • 分块粒度
      • 单块策略
      • 页面级分块
      • 模块级分块
    • 性能挑战
      • HTTP/2请求开销
      • 压缩效率下降

金句 / Highlights

值得收藏与分享的关键句。

#Next.js#Turbopack#前端优化#JavaScript分块
打开原文

Next.js 中 Turbopack 如何对 JavaScript 进行分块

返回博客

2026年9月3日,星期四

Turbopack 如何对 JavaScript 进行分块

作者:

Sam Poder

@sam_poder

打开这个页面的网络面板,你会看到一系列看似随机命名的 JavaScript 文件:

网络

正在收集请求...

点击其中一个文件,取消混淆后,你会看到类似以下内容:

36wnellv-yn9q.js

code
(
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 后,与保持独立相比,该会话的总请求数和下载代码量的变化。

导航

请求数变化

下载代码量变化

code
A

0

code
B
code
A + B

+

−1

在考虑合并块 A 和 B 时,我们需要计算仅使用 A 的块组数量、仅使用 B 的块组数量以及使用 A + B 的块组数量。我们根据每种场景发生的概率来加权合并的代价或收益。

最后,我们根据访问单页和访问多页的会话概率对这两种情况进行加权。我们估计 2/3 的会话只访问单页,而 1/3 的会话涉及两个或更多页面。

三种分块策略

这是我们讨论过的分块策略的对比。

为了生成这些数据,我使用三种不同的 Turbopack 分块配置访问了 nextjs.org:永不合并、默认配置、以及在每个块组内合并所有块。

在每个版本中,我运行了相同的导航序列,测量请求数和下载的客户端 JavaScript 体积。

无合并

Turbopack 默认

每组一个块

1

code
nextjs.org

(初始页面加载)

363.6 KiB(76 个请求)

344.2 KiB(24 个请求)

315.3 KiB(6 个请求)

2

code
/blog

35.0 KiB(4)

35.0 KiB(3)

34.0 KiB(2)

3

code
/blog/next-16-3-turbopack

6.0 KiB(2)

8.7 KiB(1)

38.3 KiB(1)

4

code
/learn

7.4 KiB(5)

6.6 KiB(2)

5

code
/learn/dashboard-app

109.6 KiB(3)

115.0 KiB(3)

142.0 KiB(1)

6

code
/learn/dashboard-app/getting-started

0 KiB(0)

7

code
/showcase

24.6 KiB(2)

25.5 KiB(2)

25.0 KiB(1)

8

code
/docs

15.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 最近关于代码块权衡与限制的演讲。