Bun 1.4 与 Node.js 24 运行 Next.js standalone 极限压测

太阳作者太阳
原创内容采用 CC-4.0 协议发布,转载请注明出处

我把博客的 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.3autocannon 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%

在博客平时几乎没有并发访问的前提下,这三档已经远超实际需求。继续测试纯粹是为了找到饱和点。

逐级加压,而不是一开始就打满

极限测试按 501002004008001600 个连接逐级增加,每档持续 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 MiB580.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%”。曲线告诉我:实际博客不需要多进程监听同一个端口,更不需要为了不存在的并发问题增加一套进程管理。