AfterPack 性能:实测速度与体积

AfterPack 对构建和页面的代价:在 185 KB bundle 上实测的混淆耗时、wasm 构建的耗时与下载体积、每个预设的输出体积、页面启动时间和运行时代价、受保护代码读取宿主全局对象的开销,以及新种子对缓存的影响。

本页的每一个数字都是实测所得,而不是估算。除非表格另有说明,测试机器均为 Apple M2 Max、32 GB 内存、macOS 26.6.2。这里没有任何内容是与其他工具的对比。

大部分数据背后的测试样本是 next16_framework_runtime:一次真实的 Next.js 16 构建所交付的框架运行时,共 185 KB。它是一个已经压缩过的生产 bundle,这正是 AfterPack 实际处理的形态。

构建耗时

2026-09-29 在上述机器上测得,使用 AfterPack 0.2.0,采用默认的 light 预设。

运行内容输入耗时吞吐量
npm 包,处理 185 KB bundle185 KB229 ms0.79 MB/s
单独运行引擎,同一个 bundle185 KB243 ms0.74 MB/s
npm 包,处理 766 KB 的多文件构建766 KB1,362 ms0.55 MB/s

规划时应以 npm 那一行为准。它是已发布的包按照插件或 npx afterpack@latest 的方式完成工作,因此包含了把代码送入引擎再取回的开销。引擎那一行是同一个 bundle 在同一个预设下去掉这部分开销后的结果,由另一套独立的测试工具计时。两者相差不超过 15 ms,因此打包封装没有带来可测量的开销。

引擎那一行在处理单个 185 KB 文件时,峰值内存比基线高 20.8 MB。766 KB 的多文件运行在最高点占用了 76 MB。

混淆是构建后的步骤,因此这段时间加在你的构建上,而不是加在用户的页面加载上。766 KB 的构建大约耗时 1.4 秒,这就是默认预设下开销的量级;更高的预设开销更大。

wasm 构建

@afterpack/wasm 是同一个引擎编译成 WebAssembly 的版本,用于 Cloudflare Worker、Node,以及本站的 playground。

耗时(按预设)Node 24
110 KB bundle,light147 ms
185 KB bundle,light237 ms
185 KB bundle,medium1,011 ms
185 KB bundle,extreme2,603 ms

每个耗时都是开启 Protection Map 时九次运行的中位数,于 2026-10-17 使用 @afterpack/wasm 0.2.6 在上述机器上测得。110 KB 的 bundle 是同一个 Next.js 16 构建中的 polyfills 分块。

下载体积原始为 4.36 MiB,gzip 压缩后为 1.25 MiB。下载在每个 Worker isolate 或每个浏览器标签页中只发生一次,而不是每次构建一次。

速度是强制执行的:一项 CI 检查会在 185 KB 测试样本上以 extreme 预设并开启 Protection Map 为引擎计时,同一次运行中还会为一个固定版本的参考引擎计时;如果引擎最快的一次运行耗时超过参考引擎的 13.5 倍,就让构建失败。

wasm 的运行速度比原生构建慢,它的速度是一个有意做出的取舍:早先一个针对体积优化的构建在 gzip 后小了 39%,但慢了 2.1 倍,导致一个真实的 66 文件构建超出了云端引擎的时限。最终速度胜出。

这些是笔记本上的耗时,不是 Cloudflare 上的耗时。Cloudflare 网络上的 Worker isolate CPU 更少、缓存更冷,还有单请求的 CPU 限制,因此同样的输入在那里的耗时会比这张表显示的更长。在用这些数字来规划预算之前,请先在你将要运行的平台上实测。

输出体积

混淆会增加字节。于 2026-10-16 使用 AfterPack 0.2.6,在五个测试样本(上面的 185 KB 运行时、另外两个 Next.js 16 chunk、一个 Vite React bundle,以及一个 PostHog chunk)上按预设测得,比较的是 gzip 后的输出与同一文件压缩后的版本:

预设gzip 后的输出倍数
minify1.09x 至 1.17x
light1.29x 至 1.60x
medium6.04x 至 8.33x
hard6.62x 至 9.23x
extreme8.25x 至 11.19x

每个范围是各测试样本之间的分布,而不是置信区间,并且随预设单调递增。

复杂度目标决定输出体积;上面的范围是各预设实际产出的测量结果。当你想要构建在超出你选择的体积时失败,而不是继续增长,就设置显式的 inflation.max;参见预设。

小文件的输出按比例会比上面任何一个数字都大,因为每份输出都带有引擎固定的运行时开销。请在真实的 bundle 上读取比值。

页面加载

更重的预设会让页面启动更慢,因为浏览器在页面响应之前要解析和运行更多代码。于 2026-10-24 使用 AfterPack 1.0.0,在上述机器上的无头 Chromium 中测得:五个来自 AfterPack 插件端到端测试的小型应用,每个应用不使用 AfterPack 构建一次,再按每个预设各构建一次,整个 bundle 都使用该预设,并从 localhost 提供服务,因此不包含任何网络耗时。每个数字是从导航开始到第一次点击应用计数器并使其发生变化的时间,取五次冷加载的中位数,每次都在全新的浏览器上下文中进行。

应用不使用 AfterPacklightmediumhardextreme
Vite 和 React35 ms52 ms253 ms295 ms596 ms
Vite 上的 Vue31 ms36 ms176 ms117 ms199 ms
Astro islands44 ms48 ms205 ms249 ms622 ms
SvelteKit27 ms32 ms84 ms89 ms162 ms
Next.js,webpack42 ms74 ms462 ms573 ms1,010 ms
Next.js,webpack,CPU 降速 4 倍145 ms234 ms1,652 ms2,215 ms3,844 ms

最后一行把 Chromium 的 CPU 降速为四分之一,粗略模拟一部中端手机。

在保护之前,这些页面加载 168 KB 至 453 KB 的 JavaScript。体积在每个应用上的增长倍数大致相同(比值见预设);启动时间的增长取决于第一次点击之前有多少这类代码要运行。在 medium 下,主线程脚本耗时在 Vue 上是未保护构建的 4.5 倍,在 Next.js 上是 14.7 倍。框架自身的运行时也是每个 bundle 的一部分,每个预设都会同样保护它。

在 light 下,第一次有效点击晚了 4 ms 至 32 ms。从 medium 起,在把该预设用于整个 bundle 之前,请先实测你自己的页面;每个层级允许你限定哪些范围,见预设。

运行时代价

受保护的代码比原始代码运行得慢,代价随预设升高。使用 AfterPack 0.2.4 在自托管的 x86_64 CI 运行器上、Node 22 下测得:四个紧密循环程序,每个数字是受保护程序的运行时间除以原始程序的运行时间,取四者的中位数。

预设相对原始代码的运行时间
minify1.0x
light2.1x
medium32.5x
hard35.0x
extreme37.3x

这些是最坏情况:这些循环只做受保护的算术运算,而普通应用代码花在这类循环上的时间要少得多。如果某个预设的中位数超过这里数字的 2.5 倍,CI 检查就会让引擎构建失败。

热点数值内核,例如图像滤镜、物理步进或解析器的内层循环,付出的代价接近完整数字。有 Pro 密钥时,用区域预设把它保持在 light,bundle 的其余部分保持全局预设。Free 构建会忽略更低的区域预设,所以在 Free 上,如果热点内核很重要,就把全局预设保持在 light。

宿主全局变量的读取

受保护的代码通过一个检查来读取 URL 这样的宿主全局变量;当该全局变量不存在时,这个检查会抛出与原始代码相同的 ReferenceError。这个检查的开销取决于代码在哪里运行。

于 2026-10-13 使用 AfterPack 0.2.4,在 arm64 Mac 上的 Node 24 中,用一个每次迭代读取一次 URL 的循环测得。每个数字是受保护构建的运行时间,除以同一个受保护构建不经过该检查直接读取全局变量时的运行时间。每个范围覆盖两个种子,每个种子取 5 到 7 轮交错运行中最快的一轮。

预设普通上下文node:vm 内
light1.00x2.7x
medium1.06x 至 1.13x2.0x 至 2.1x
hard1.04x 至 1.05x1.9x 至 2.1x
extreme1.10x 至 1.18x2.0x 至 2.1x

普通上下文指浏览器页面、worker 或 Node 的主上下文;这一列在 Node worker 线程中测得。在这里,该检查在 light 下没有可测量的开销。从 medium 起,预设附加的保护会包裹这个检查,每次读取都要付出这部分开销。

node:vm 上下文对其全局对象的每一次查找都会经过一个回调,因此在这里,同一个循环的耗时是不经过该检查时的 1.9x 到 2.7x。在 node:vm 中运行受保护代码的测试或基准测试看到的是这项开销的上限,因此对于在热循环中读取宿主全局变量的代码,请在用户实际运行它的地方测量。

跨发布的缓存

新的种子会改变每个受保护 chunk 的字节,包括源码没有变化的 chunk。打包工具插件会把种子计入每个 chunk 的文件名,所以每次发布后,回访用户都要重新下载全部受保护的 JavaScript。固定的种子能让没有变化的 chunk 保持逐字节相同并继续命中缓存,但这个 chunk 也会在各次发布间保持同一种形态。在 Next.js、Angular、Vite 或 webpack 的输出上,CLI 会像插件一样重命名带内容哈希的文件;其他输出的文件名保持不变,此时请固定 seed,或给这些输出设置较短的缓存时间。详见文件名与缓存。

下一步

  • 预设:每个预设设定的体积预算,以及提高预设的代价。
  • 构建与 CI:这一步在流水线中应该放在哪里。
  • Cloudflare Workers:在 isolate 中运行 wasm 构建。
  • Playground:在你的浏览器中运行、并为自己计时的 wasm 构建。