
我有一个由 20 个测试文件组成的测试套件运行一次需要 4 秒多。我只加了一个参数--parallel耗时就降到了 1 秒。接着我尝试进一步压榨性能并找到了这个参数停止生效的临界点——事实证明这取决于测试实际执行的工作类型而不是文件的数量。Bun 1.4 于 2026 年 8 月 20 日发布带来了bun test --parallel它会将测试文件分发到多个 worker 进程中并行执行而不是在单个进程里逐个运行。我在一台 4 核 Intel Xeon 虚拟机上KVM4 个逻辑 CPU无 cgroup 限制运行 Bun 1.4.2我想弄清楚这个参数到底能带来多少收益、它的上限在哪里以及文档中隐含--isolate这句话在实际中意味着什么。我写了 20 个测试文件每个文件两个测试其中每个测试都会先await一个 100ms 的setTimeout再执行断言——用来模拟拖慢大量真实集成测试的 API 调用和防抖逻辑。每组运行三次最快与最慢之间的差距在 15ms 以内模式墙钟耗时bun test串行4.04s--parallel14.06s--parallel22.05s--parallel4默认值 CPU 核心数1.03s--parallel80.63s不带参数时--parallel默认使用 CPU 核心数在这台机器上是 4。仅此一项就把套件从 4.04s 降到了 1.03s提速 3.9 倍这与 4 个 worker 分摊 20 个文件的情况几乎完全吻合。让我惊讶的是在这台只有 4 个逻辑核心的机器上--parallel8仍然在继续提升降到了 0.63s。我稍后会解释原因因为这是本文最重要的一部分。另一面的读数CPU 密集型测试会在核心数处撞墙一个await setTimeout的测试在等待时其实并不占用 CPU它只是占着一个坑位。于是我构建了第二个套件结构相同20 个文件、每个文件 2 个测试但这次每个测试运行真实计算一个筛选到 2000 万的质数筛prime sieve它占用真正的 CPU 时间没有任何等待。模式墙钟耗时bun test串行6.1s--parallel16.2s--parallel23.3s--parallel41.8s--parallel61.9s--parallel81.8s这个数字才是反对头条结论的证据。扩展到 4 个 worker 时缩放几乎呈线性与 4 个物理核心匹配然后就不再提升了。增加到 6 个或 8 个 worker 没有任何收益在某些轮次中甚至比 4 个稍慢——大概是进程争夺相同核心时产生了上下文切换开销。--parallel调度的是操作系统进程它并不能凭空变出更多 CPU。对于真正受 CPU 限制的套件关键数字不是你有多少个文件而是nproc。把两张表放在一起真正的规律比任何一张单独的表都更简单对于大部分时间在等待的测试--parallel可以扩展到超过你的核心数而对于大部分时间在计算的测试它做不到。大多数真实套件是两者的混合所以诚实的建议是对自己的套件做基准测试而不是假设它符合某一条曲线。隐含--isolate到底隔离了什么文档说--parallel隐含--isolate并将其描述为即使两个文件落在同一个 worker 上每个文件也会获得全新的全局对象。我想确切知道它重置了什么因为这种措辞没有说清是按文件隔离还是更粗或更细的粒度。我写了一个带有顶层计数器的模块和四个导入它并打印所见值的测试文件// shared.ts let count 0; export function bump() { count; return count; }在普通bun test下整个运行共享一个进程和一个模块缓存因此计数器会按文件实际运行的顺序跨文件递增file-4 sees counter 1 file-1 sees counter 2 file-2 sees counter 3 file-3 sees counter 4在--parallel下每个文件看到的都是1file-1 sees counter 1 worker 1 file-2 sees counter 1 worker 1 file-3 sees counter 1 worker 1 file-4 sees counter 1 worker 1四个文件都在同一个 worker 进程中运行相同 PID所以这与操作系统进程隔离无关——而是在单个进程内每个文件拥有全新的 JS 全局对象与文档描述完全一致。在--parallel上加上--no-isolate泄漏立刻回来了又变成了1, 2, 3, 4与串行相同。那一行文档摘要没有说清楚的部分是隔离是按文件而不是按测试。同一个文件内的三个测试在--parallel下运行时计数器会依次看到1, 2, 3之间没有重置。如果你的测试依赖模块级设置在每一个测试而不仅是每个文件都重新初始化那么--isolate无法提供这一点无论是否并行。Bun 还会把BUN_TEST_WORKER_ID和JEST_WORKER_ID设置为 worker 的从 1 开始的索引我在上面的测试中打印两者确认过——如果你在移植一个以该变量作为数据库名或端口键的 Jest 配置这一点值得了解。--bail停止的是尚未启动的文件而不是正在运行的文件文档描述得很精确协调器按文件粒度处理--bail一旦达到失败阈值它就不再启动新文件但已在运行的文件会继续跑完。我针对 20 个文件测试了这一点其中第一个文件有一个耗时 300ms 的失败测试。不加--bail在--parallel下无论是否失败20 个文件全部运行19 pass 1 fail Ran 20 tests across 20 files. [1.53s]加上--bail后Bailed out after 1 failure 3 pass 1 fail Ran 4 tests across 4 files. [320.00ms]运行了 4 个文件而不是 1 个。这与失败发生时已有 4 个 worker 正在执行相匹配其余 16 个文件从未启动。这在 CI 中能节省大量时间这里从 1.53s 降到 0.32s但这也意味着在--parallel下使用--bail你无法得知那 16 个未触碰的文件中是否还有其他失败——包括那些与导致首次失败的问题毫无关系的失败。覆盖率合并正确、非法输入被干净拒绝、单个坏文件不会拖垮整个运行我还检查了另外三件事简要说明覆盖率合并我把一个模块的三个函数拆分到三个测试文件中每个文件覆盖一个函数并比较了--coverage输出。串行与--parallel --coverage报告的数值完全相同——函数覆盖率 75%、行覆盖率 60%、未覆盖行范围一致——因此文档所称的跨 worker 覆盖率合并完全成立。非法输入在任何测试运行前就被拒绝--parallel0、--parallel-1和--parallelabc都产生了同样简洁的错误信息和退出码 1且没有执行任何内容error: --parallel expects a positive integer, received 0一个文件的崩溃不会拖垮整个运行我有一个测试文件在模块加载时抛错与文件内任何测试无关。在--parallel下该批次中的另外三个文件仍然运行并通过崩溃被报告为独立的 error而不是与断言失败混在一起bun test v1.4.2 (744846f84) 4x PARALLEL tests-crash/cr2.test.ts: # Unhandled error between tests ------------------------------- error: boom at module load time ------------------------------- 3 pass 1 fail 1 error Ran 4 tests across 4 files. [11.00ms]横幅中那行4x PARALLEL也是从 CI 日志确认实际运行了多少 worker 的最简单方式无需向自己的测试代码添加任何内容。我在这条路上犯过的错我第一次构建CPU 密集型套件时用while (Date.now() - start 100) {}来模拟真实工作期望它的表现和质数筛套件一样。结果并非如此在一台 4 核机器上--parallel8一直持续变快远超真实 CPU 工作应该达到的平台期形状与 IO 密集型套件相同。我花了一段时间假设 Bun 在调度上做了什么聪明的事才想明白真正的原因一个检查墙钟时间的自旋循环只需在截止时间之后被调度一次就会注意到时间已过并退出。它表现得像 sleep 而不是计算因为操作系统想抢占它多久就多久循环根本不在意——只要它下次被调度检查时真实时间已经流逝即可。这就是我重建 CPU 密集型套件时改用真正的质数筛、完全不检查时钟的原因也就是上面表格中的版本。自己动手跑一遍这是生成第一张表的精简版脚本。需要 Bun 1.4 或更高版本。curl -fsSL https://bun.sh/install | bash mkdir bun-parallel-demo cd bun-parallel-demo for i in $(seq -w 1 20); do cat t$i.test.ts EOF import { test, expect } from bun:test; test(io-$i, async () { await new Promise((r) setTimeout(r, 100)); expect(1 1).toBe(2); }); EOF done bun test # 串行基线 bun test --parallel # 默认使用你的 CPU 核心数 bun test --parallel8 # 超出核心数继续压榨因为这些测试只是在等待把生成的测试主体替换为真实计算一个质数筛、一次排序任何不带定时器的东西就能看到平台期而非持续扩展并与你自己机器上的nproc对比。该怎么用在为现有套件开启--parallel之前先弄清楚你的套件到底属于哪种类型。如果你的大部分测试都在等待定时器、socket 或数据库往返把--parallel提高到超过核心数就是免费的速度提升值得立刻尝试。如果你的测试在进程内做真实计算那么超过nproc就没有任何收益应该把--parallel设为该数值而不是猜一个更大的数。无论哪种情况在依赖--isolate保证正确性之前先检查你的测试在模块作用域到底共享了什么它重置的是文件之间的状态而不是同一文件内测试之间的状态而这个缺口正是测试套件变得不稳定的常见温床。相关阅读延伸外链以下为推荐的相关技术教程来自致知笔记如何将笔记本电脑连接到外接显示器如何检查 IP 地址是静态还是动态Static/Dynamic IP如何在 Windows 11/10 中创建密