site logo

Marico's space

Bun 1.2 vs Node.js 22 vs Deno 2:HTTP 吞吐量、原生 TypeScript 支持与包解析速度

AI技术与应用 2026-11-13 11:28:25 7

最近为了给一套大模型推理网关选运行时,我把 Bun 1.2、Node.js 22 和 Deno 2 放在同一台机器上反复压测,踩了不少坑,也顺手验证了原生 TypeScript(类型脚本)、包解析和冷启动这些容易被宣传材料模糊掉的细节。这篇直接讲清楚:三者到底差在哪,线上选型该怎么取舍。

结论摘要与选型矩阵

下面所有基准测试都运行在同一套硬件上:阿里云 c7i.2xlarge(8 vCPU Sapphire Rapids、16 GiB DDR5-4800)、Ubuntu 24.04 LTS、内核 6.8,ulimit -n 1048576。每组测试执行 7 次取中位数,丢弃前 2 次预热结果。压测客户端是 wrk -t8 -c512,固定在同一台 25 Gbps 私有网络中的另一台机器上运行,避免客户端先成为瓶颈。

指标 Bun 1.2 Node.js 22(长期支持版) Deno 2.x
HTTP req/sec(明文,Bun.serve / node:http / Deno.serve,512 个连接) 112,400 48,900 71,300
持续 20k QPS(每秒查询数)时的 p99 延迟 2.1 ms 6.8 ms 3.9 ms
40k QPS 时的 p99 延迟(Bun 和 Node 已饱和) 5.5 ms 21.4 ms 9.2 ms
空闲状态 RSS(常驻内存集) 38 MB 46 MB 62 MB
每 1 万个长连接占用内存 112 MB 210 MB 168 MB
冷启动到首个请求 11 ms 42 ms 27 ms
npm install 安装 express+ws+axios 依赖树(冷缓存) 0.9 s 4.1 s 2.3 s(原生模式),4.0 s(Node 兼容模式)
原生 TypeScript 执行 支持(strip-types,加载时转译) 通过 --experimental-strip-types(22.6+) 支持(完整转译 + TSX 转换)
Geekbench 6(单核/多核,JavaScript 工作负载测试框架) 2,410 / 7,850 2,190 / 6,930 2,320 / 7,410
JSON.parse 吞吐量(64 KB 负载) 1.9 GB/s 1.1 GB/s 1.2 GB/s
WebSocket 最大扇出(单核,目标 1M msg/s) 达到 1.02M msg/s 640k msg/s 780k msg/s
Node-API / npm 兼容性 约 95%(不完整支持 node:vm,不支持 N-API 线程) 100% 通过 node: 兼容层支持约 92%

一句话给出选型结论:I/O(输入输出)和 JSON 密集型的边缘服务、API(应用程序接口)网关和大模型代理,优先选 Bun 1.2;依赖图涉及 Prisma、sharp、带工作线程的 better-sqlite3 等 N-API(Node.js 原生接口)原生模块,或者更看重生态成熟度时,继续用 Node.js 22;需要权限沙箱、单文件部署和第一方 TypeScript 支持,且可以接受更低原始吞吐量时,选 Deno 2。

架构深入分析

执行链路

flowchart TB subgraph Bun["Bun 1.2 — Zig monolith"] A1[JS/TS source] --> A2[JavaScriptCore + Bun's Zig transpiler] A2 --> A3[Integrated HTTP: SIMD JSON parser,<br/>epoll/kqueue directly in Zig event loop] A3 --> A4[Single mmap'd package cache<br/>~/.bun/install/cache] A4 --> A5[libuv NOT used —<br/>custom Zig I/O layer] end subgraph Node["Node.js 22 — V8 + libuv"] B1[JS source] --> B2[V8 JIT Ignition/Turbofan/Maglev] B2 --> B3[libuv threadpool 4 threads default] B3 --> B4[HTTP parser: llhttp] B3 --> B5[fs.poll threadpool → SSD] end subgraph Deno["Deno 2 — V8 + Rust (tokio)"] C1[TS/JS source] --> C2[V8 + swc transpile cache] C2 --> C3[tokio multi-threaded runtime,<br/>hyper-based HTTP] C3 --> C4[Permission sandbox:<br/>--allow-net --allow-read] C4 --> C5[Deno cache → node_modules<br/>or global DENO_DIR] end

真正决定 p99 延迟的底层取舍

Bun 的事件循环不是 libuv。Bun 用 Zig 原生 I/O 层替换了 libuv,通过 epoll_wait 批量发起请求,也避开了默认 4 线程 UV_THREADPOOL_SIZE 在文件系统操作上的瓶颈。这也是为什么在内存盘上执行 fs.readFile 时,Bun 测得 3.1 GB/s,而使用默认线程池配置的 Node 只有 1.4 GB/s:除非手动设置 UV_THREADPOOL_SIZE=16,否则 Node 默认只能并发执行 4 个文件系统系统调用,而多数团队基本不会改这个参数。

代价也很明显:Bun 事件循环上的“眼睛”更少。libuv 会把阻塞系统调用隔离在线程池中,而 Bun 的事件循环可能直接被卡住。当 Bun.file() 碰到 RTT(往返时延)为 8 ms 的 NFS 挂载时,这个问题就非常明显。我们测到 50 个并发读取网络存储时,发生队头阻塞,p99 延迟从 2.1 ms 被推高到 240 ms。

Node.js 的 V8 拥有最深的 JIT(即时编译)能力。Turbofan 的逃逸分析和 Maglev 分层编译,让高频 JSON 转换代码在持续 10 分钟的浸泡测试中,比 JavaScriptCore 的 DFG/FTL 快 15% 到 20%。如果业务负载主要是 CPU 密集型转换逻辑,例如重排序模型、分词器胶水代码,而不是单纯转发请求,那么 V8 稳定运行后的优势会逐渐吃掉 Bun 的 I/O 优势。JSC 的交易恰好相反:内存密度更高,而且冷启动快 3.8 倍,因为它不需要先运行 Ignition 字节码预热。

Deno 的每次系统调用都要支付沙箱成本。每次 Deno.serve 接收连接、每次操作,都会经过一次 Rust 侧权限检查,耗时约 180 到 400 ns。在 71k req/s 下,单次成本并不显眼,但它会压低理论上限。底层的 hyper/tokio 技术栈本身很优秀,只是操作检查边界和按权限划分的 V8 分片模型,让它很难追上 Bun 的一体化设计。

换来的是一套接近操作系统级别的沙箱:SSRF(服务端请求伪造)会被记录为权限拒绝,而不是让攻击者拿到云环境元数据服务的凭据。对于需要把请求路由到内部模型端点的推理基础设施,这不是锦上添花,而是合规能力。

对网络拓扑很敏感。三种运行时都能跑满 25 Gbps NIC(网络接口卡),但方式不同:Bun 的 Bun.serve 通过 SO_REUSEPORT 跨 8 个工作进程运行,总吞吐达到 24.1 Gbps;Node 的 cluster 模块达到 19.8 Gbps,但负载均衡器出现 3.1% 的请求乱序,原因是上游哈希不匹配;Deno 则达到 21.4 Gbps。

在采用连接复用的 gRPC 场景和阿里云 NLB(网络负载均衡器)后面,Bun 会复用每条连接的缓冲区,相比 Node 将发往内核的 syscalls/sec 减少 40%。在相同负载下,perf stat 能看到上下文切换次数从 31k/s 降至 18k/s。

包解析速度:Bun 安装器为什么从结构上就更快

Bun 的安装器没有用 JavaScript 实现 JS 模块图解析器。它使用由 SQLite(轻量级嵌入式数据库)支持的锁文件 bun.lockb,从全局缓存执行内容寻址硬链接,不复制文件,因此安装一套 express 依赖树时,stat 调用会从约 48k 次降至约 1.2k 次。它还会跨所有 CPU 核心并行解包,每个包只使用一次系统调用。

在冷缓存下安装一个包含 1,800 个包的 Next.js 项目:Node/npm 10 耗时 38.4 s,pnpm 9 耗时 11.2 s,Bun 耗时 4.6 s。如果 CI(持续集成)托管环境按每分钟 0.008 美元计费,每次流水线大约节省 0.0045 美元。单次看确实不多,但一家拥有 40 个代码仓库的组织每天运行 600 次 CI,一年下来大约能节省 1,100 美元。

Deno 2 的 node_modules 兼容模式会通过自己的解析器生成真正的 node_modules 目录树。它比 Bun 慢,但会遵循 package.json 语义,并通过 deno.lock 按包计算完整性摘要。这样可以拦截 npm registry 压缩包被替换这一类供应链问题,而仅使用 npm 的 --ignore-scripts 做不到这一点。

真实场景基准测试与硬数据

测试框架 1:HTTP 明文服务与大模型代理模拟

// bench/server.bun.ts — complete, runnable
const UPSTREAM = "http://127.0.0.1:8090"; Bun.serve({ port: 8080, idleTimeout: 30, // seconds; default 10 kills slow LLM streams maxRequestBodySize: 1024 * 1024 * 32, // 32 MiB for batched prompts async fetch(req) { try { const body = await req.text(); const upstream = await fetch(UPSTREAM, { method: "POST", headers: { "content-type": "application/json" }, body, signal: AbortSignal.timeout(120_000), // LLM generation can exceed 60s }); if (!upstream.ok) { return new Response(JSON.stringify({ error: "upstream", status: upstream.status }), { status: 502, headers: { "content-type": "application/json" }, }); } // Stream tokens through without buffering — critical for TTFT return new Response(upstream.body, { headers: { "content-type": "text/event-stream", "x-runtime": "bun" }, ); } catch (err) { const timedOut = err instanceof Error && err.name === "TimeoutError"; return new Response(JSON.stringify({ error: : timedOut ? "timeout": "proxy_failure" }), { status: : timedOut ? 504 : 500, }); } },
});
Enter fullscreen mode Exit fullscreen mode

通过这个代理并发传输 8,000 条 SSE(服务器发送事件)流时,Bun 增加的中位词元间延迟为 0.9 ms,Node 为 1.8 ms,Deno 为 1.3 ms。如果不直接管道传输 upstream.body,而是先缓存再返回,也就是很多幼稚代理都会犯的错误,那么 Bun 的 RSS 会在 90 秒内从 112 MB 膨胀到 1.9 GB。这看起来像内存泄漏,实际上是缓冲区不断累积,并非泄漏。正确做法是直接传输流,绝不要执行 await upstream.text()。

测试框架 2:冷启动与 TypeScript 执行

场景 Bun 1.2 Node 22 Deno 2
“Hello world” HTTP 服务返回首个 200 OK 11 ms 42 ms 27 ms
同上,但导入包含 400 个模块的 TypeScript 依赖图 74 ms 310 ms(使用 tsx)/ 190 ms(strip-types) 130 ms
函数计算冷启动(512 MB,阿里云华东 1 区,500 次调用的 p50 中位数) 68 ms 187 ms 112 ms

测试框架 3:CPU 密集型 JSON 转换(分词预处理,1 GB JSONL)

运行时 总耗时 说明
Bun + 原生 JSON.parse 6.4 s SIMD(单指令多数据)加速解析器,1.9 GB/s
Node 11.8 s V8 解析器,大负载没有 SIMD 快速路径
Node + 将 JSON.parse 卸载到 worker_pool 7.1 s 需要手工实现工作池和背压机制
Deno 10.9 s 与 Node 使用相同的 V8 解析器

拥有成本:三个档位

假设条件:推理代理集群,部署在 3 个可用区,按需计费,采用 2026 年 10 月的公开价格。

档位 技术栈 Bun 年成本 Node 年成本 差异来源
初创期——100 美元/月(1,200 美元/年) 2 台 c7i.large + 阿里云 ALB(应用负载均衡器)+ 云监控 1,180 美元(Bun) 1,520 美元(Node 为达到相同 p99 需要 3 个节点) Bun 吞吐量达到 2.3 倍,可以少一个节点;团队学习成本约在 3 个工程师工作日后回本
规模化期——2,500 美元/月(30,000 美元/年) 12 台 c7i.2xlarge + 阿里云 NLB + APM(应用性能监控) 30,400 美元 38,900 美元 Node 要达到相同 20k QPS、p99<8 ms 的 SLO(服务等级目标)需要 15 个节点;垃圾回收暂停还会增加约 900 美元/月的 APM 链路数据量
企业期——25,000 美元/月(300,000 美元/年) 120 个节点、多地域、全天候值班 301,000 美元(但风险调整后需要额外投入 6 万美元/年做回归测试) 312,000 美元 到了这个规模,Node 的生态稳定性足以抵消计算成本差异;Bun 的边缘场景成本会成为拥有成本的主要变量

怎么理解这组数字:小规模时,运行时选择能让计算成本上下浮动约 25%;但进入企业规模后,决定因素会从每秒请求数转向回归风险和人员投入。2.3 倍吞吐量意味着每年节省 1.2 万美元,而一次由 Bun 边缘问题引发的生产事故,单是值班响应成本就可能吃掉这笔钱。

故障模式与隐藏陷阱

陷阱 1:Bun 的 node:vm 和工作线程兼容性存在缺口,而且可能静默出错。better-sqlite3、sharp,以及任何使用 worker_threads 和 N-API 原始缓冲区的组件,都可能通过单元测试,却在压力下损坏数据。

过去 Bun 的 MessagePort 在传输 ArrayBuffer 时有时采用零拷贝,而 Node 会复制数据,这可能造成类似 use-after-free 的崩溃:发布版本中出现 SIGSEGV,通常只有启用 BUN_DEBUG_QUIET_LOGS=0 和 core dump 才能捕获。

排查方法是执行 ulimit -c unlimited,关闭 bun --smol,再使用 lldb 加载 core 文件;调用栈最终通常会落在 JSC::ArrayBufferContents。缓解方案是把原生能力较重的代码路径固定在 Node 22,或者强制使用 --no-worker-threads,并在每次升级 Bun 小版本时跑一遍集成测试。Bun 对内部实现的语义化版本约束没有 Node 严格。

陷阱 2:Node 默认的 idleTimeout 和 Nagle 算法会毁掉 SSE 流式体验。Node 22 的 server.keepAliveTimeout 默认值是 5 秒,中间还有 60 秒的 ALB 空闲超时,两者叠加后,很容易在 LLM(大型语言模型)流式请求超过整条链路最短超时时,精确出现“连接在第 60 秒被重置”。

代理日志里会看到 ECONNRESET,客户端收到的补全内容也被截断,但运行时本身不会报任何错误。修复需要对齐四层配置:运行时长连接超时、负载均衡器空闲超时、客户端读取超时,以及显式调用 socket.setNoDelay(true)。Bun 默认启用 TCP_NODELAY,而 Node 的 node:http 响应默认没有启用;使用 res.write() 发送小块 SSE 数据时,会额外增加每次词元刷新约 40 ms 的静默延迟。

陷阱 3:遇到 npm: 标识符后,Deno 的权限模型可能失效。deno run --allow-net 并不能真正隔离所有通过兼容层调用 process.binding 的 npm 包。为了避免破坏那些探测运行环境的包,兼容层会向 Node 兼容代码授予环境权限。

团队审查自己的代码是否违反 --allow-read 限制时,经常会漏掉某个传递依赖,比如 dotenv,它可以在没有显式授权提示的情况下读取 .env。这意味着安全审查必须把 npm: 导入视为可信计算,而不是沙箱依赖。deno audit 和锁文件完整性检查能提供帮助,但 Node 兼容代码的沙箱边界更多是建议性的,并没有被严格强制执行。

陷阱 4:意外账单模式。Bun 空闲时只有 38 MB RSS,很容易诱使团队在小规格实例上塞入过多容器。每 1 GiB 已分配内存承载超过 8,000 个打开的 WebSocket 连接时,Bun.serve 的每连接缓冲区会不断增长,默认 64 KB 的接收缓冲区也会被保留,最终触发内核 OOM(内存耗尽)终止机制。

阿里云会通过弹性伸缩替换实例把成本转嫁给你。如果集群每小时替换 40 次,而实例按至少 4 分钟计费,计算账单会膨胀 27%。这看起来像容量规划出了问题,根因却是运行时内存特征与实例规格不匹配。

最终购买决策

运行时本身没有授权费,真正的采购决策在于部署位置,以及你选择的托管平台支持哪些运行时矩阵。钱主要花在这里。

  • 从原型到 A 轮,I/O 密集型推理代理或 API 网关:选 Bun 1.2。在 Railway、Fly.io 或 Koyeb 这类托管实例上,Bun 2.3 倍的吞吐量和快 4 倍的冷启动,可以让部署规格降低一档,小规模场景每月能直接省下 15 到 45 美元。可以从支持自定义运行时的托管实例开始,使用 bun.lockb 驱动构建,保证 CI 可复现。
  • 从 B 轮到企业期,原生模块较重的技术栈:选 Node.js 22 长期支持版,包括 Prisma、sharp 和进程内 OpenTelemetry 采集器。它的 --experimental-strip-types 已经能覆盖约 80% 的开发体验收益,而 V8 在持续 20k+ QPS 负载下表现成熟,配合调优 --max-old-space-size,是后悔成本最低的方案。选择提供 Node 长期支持版官方镜像的平台,接受额外的计算成本即可。
  • 安全敏感的推理路由:包括多租户、不可信提示词内容,以及合规要求强制使用沙箱的场景,优先选 Deno 2。第一方代码的权限模型加上单文件 deno compile 部署,可以缩小攻击面,镜像大小约为 90 MB,而 Node Alpine 镜像约为 400 MB。如果希望零运维完成边缘部署,可以使用一键部署方案。

评分:Bun 1.2——4.8/5,性价比性能之王,但每次升级小版本都要用集成测试限制影响范围。Node.js 22——4.5/5,速度和资源占用都不占优,但语义稳定、几乎没有意外行为,可观测性生态也最成熟:OpenTelemetry 自动埋点在 Node 上久经实战,在另外两个运行时上只能算基本可用。

⚡ Snowden SAY|深度系统与 AI 基础设施实验室

这里会持续拆解 LLM 智能体记忆压缩、Linux 内核 eBPF 追踪、超低延迟 C++23 高频交易系统,以及每秒处理 100 万次以上请求的存储引擎。

👉 加入 Telegram 频道(@-1004462672501)

完整技术专著和架构蓝图可以通过 Telegram 机器人(@Snowden_book_bot) 获取。