Bun 1.4 与 Node.js 24 运行 Next.js standalone 极限压测
我把博客的 Next.js standalone 运行时从 Node.js 换成 Bun 后,最初只做了十几个并发连接的测试。结果很好看,却回答不了一个更实际的问题:两个运行时分别会在什么位置开始排队,什么时候出现明显延迟?
于是我把本地 Bun 镜像和迁移前的 Node.js 镜像放到同一台电脑上,使用同一个端口、同一份压测脚本串行测试。连接数从 10 一直加到 1600,停止条件是出现请求错误、非 2xx,或者 p99 超过 1 秒。
这不是 JavaScript 运行时的通用排行榜。它只说明我的 Next.js 博客在这台机器、这份内容和这套日志配置下的表现。
测试对象必须尽量一致
两个镜像运行的是同一套 Next.js standalone 应用,页面和内容一致,访问日志也都开启。区别只有最终执行 server.js 的程序:
tini -> bun server.js
tini -> node server.js测试环境为 Bun 1.4.0、Node.js 24.19.0、Next.js 16.3.3 和 autocannon 8.0.0。Docker 运行在 WSL2 中,可用 20 个逻辑 CPU 和约 47 GiB 内存。压测客户端访问宿主机的 127.0.0.1,再映射到容器的 3000 端口。
脚本先从首页扫描同源链接,两个镜像最终都得到 254 个 HTML 页面。这样测到的不是同一个首页被缓存后的单一路径,而是一组真实文章、分类和分页。
autocannon 是 HTTP/1.1 压测工具,支持并发连接、pipelining 和程序化 API。它的参数和结果含义单独整理在用 autocannon 给本地 Web 服务做阶梯压力测试中;工具本身的参数以 autocannon README为准。
先测正常并发
正式采样先使用 10、20、30 个连接,每档持续 30 秒,pipelining 保持为 1。
10 个连接时:
- Bun 平均
2159.31 req/s,平均延迟4.13ms,p99 为9ms。 - Node.js 平均
1227.94 req/s,平均延迟7.65ms,p99 为13ms。 - Bun 吞吐量高
75.8%,平均延迟低46.0%。
20 个连接时:
- Bun 平均
2356.34 req/s,平均延迟8.00ms,p99 为14ms。 - Node.js 平均
1466.00 req/s,平均延迟13.15ms,p99 为21ms。 - Bun 吞吐量高
60.7%,平均延迟低39.2%。
30 个连接时:
- Bun 平均
2484.94 req/s,平均延迟11.59ms,p99 为21ms。 - Node.js 平均
1483.00 req/s,平均延迟19.75ms,p99 为32ms。 - Bun 吞吐量高
67.6%,平均延迟低41.3%。
在博客平时几乎没有并发访问的前提下,这三档已经远超实际需求。继续测试纯粹是为了找到饱和点。
逐级加压,而不是一开始就打满
极限测试按 50、100、200、400、800、1600 个连接逐级增加,每档持续 30 秒。同时循环执行:
docker stats --no-stream CONTAINER每档采集 18 次左右,记录 CPU、内存和进程数。单看压测客户端的请求数不够:如果容器正在大量吃内存或后台进程已经退出,漂亮的平均值没有意义。
停止条件也要提前定好。我采用下面三个条件,任意一个出现就停止:
- 连接错误或超时。
- 出现非
2xx响应。 - p99 达到
1000ms。
这里用 p99 而不是平均延迟,是因为平均值很容易把少数已经慢到不可接受的请求藏起来。
Bun 在 100 个连接附近达到吞吐平台
Bun 从 50 个连接时的 2463.94 req/s 上升到 100 个连接时的 2592.54 req/s。再加到 200、400 和 800,吞吐量一直在 2.6k req/s 附近波动。
800 个连接时达到本次最高值:
- 吞吐量
2625.84 req/s - 平均延迟
305.77ms - p99
653ms - 最大延迟
798ms
连接数翻倍到 1600 后,吞吐量反而降至 2524.07 req/s,平均延迟升至 634.08ms,p99 达到 1272ms。服务没有报错,但已经越过停止线。
这段数据很直白:100 个连接后继续增加并发,没有换来更多吞吐,只是在排队。
Node.js 在 800 个连接达到停止条件
Node.js 在 50 到 100 个连接时已经接近自己的平台:
- 50 个连接为
1559.84 req/s - 100 个连接为
1606.57 req/s - 200 个连接为
1613.34 req/s - 400 个连接达到最高的
1653.37 req/s
到 800 个连接时,吞吐量回落到 1573.37 req/s,平均延迟为 504.78ms,p99 达到 1081ms,最大延迟为 4427ms。因为已经超过 1 秒,我没有继续测 1600 个连接。
高压结束后健康检查仍返回 200,整个过程也没有连接错误或非 2xx。这里观察到的是 CPU 饱和后的排队,不是应用崩溃。
吞吐量、CPU 和内存放在一起看
两个容器的 CPU 都长期停在约 100%,说明这组 Next.js 工作主要受一个逻辑核心限制。这个结果也解释了为什么增加连接数只能推高延迟。
本次最高实测吞吐量:
- Bun:
2625.84 req/s - Node.js:
1653.37 req/s - Bun 高
58.8%
极限阶段采集到的最高容器内存:
- Bun:
1077.2 MiB - Node.js:
2161.7 MiB
内存采样是离散的 docker stats 快照,不是堆分析,不能推导垃圾回收机制或内存泄漏。它能说明的只有:在本次连续加压中,Node.js 800 连接那一档出现了更高的容器内存峰值。
镜像大小和启动时间
docker image inspect 返回的镜像大小为:
- Bun:
77.8 MiB - Node.js:
93.0 MiB - Bun 小
16.3%
从 docker run 到健康检查首次返回 200:
- Bun:
1585ms - Node.js:
1618ms
33ms 的差距很容易被 Docker 创建容器和端口映射抖动吞掉,我不把它算作 Bun 的启动优势。
空闲和普通压测结束后的内存也没有明显优势。首次健康检查后 Bun 为 143.4 MiB,Node.js 为 135.5 MiB;三档普通压测结束后分别是 594.4 MiB 和 580.1 MiB。只挑极限峰值而不写这些数据,会让结论失真。
两次试跑让我改了测试方法
第一次试跑时,WSL 空闲退出顺带停止了 dockerd,容器收到 SIGTERM。一开始我以为 Bun 或 Next.js 静默崩溃,检查容器状态后才看到退出码是 143,并且没有 OOM。正式测试期间我保持 WSL 会话存活,两个镜像才完成相同条件的采样。
另一个问题来自重定向。蜘蛛把重定向前的地址加入了列表,而 autocannon 不会自动跟随这些跳转,于是出现非 2xx。修复方式是保存 fetch 最终响应地址,再交给压测器。
这两次失误没有写进正式结果,却应该写进方法。否则下次很容易把环境退出当成运行时故障,把重定向当成服务错误。
这份结果能说明什么
在这份 Next.js standalone 博客上,Bun 1.4 的吞吐量和高并发延迟明显好于 Node.js 24。我会继续让生产容器使用 Bun。
但测试主要覆盖静态生成页面和 SSG 页面返回,不代表搜索 API、MCP、附件下载或经过公网反向代理后的性能。访问日志也一直开启。如果目的是测运行时理论上限,还需要增加关闭日志的对照组,并把压测客户端移到另一台机器,排除客户端与服务端争抢 CPU 的影响。
我更看重这次得到的饱和曲线,而不是一句“Bun 快 58.8%”。曲线告诉我:实际博客不需要多进程监听同一个端口,更不需要为了不存在的并发问题增加一套进程管理。