Bun vs Node.js:别被 benchmark 骗了,你的瓶颈可能根本不在运行时
软件工程不只是把一个 div 居中这么简单。
Bun 在 benchmark 里会赢。但你的应用真正卡住的地方,通常是数据库连接、阻塞性的 CPU 任务,以及藏在循环里的 N+1 查询。如果这些问题还没解决,先换运行时,其实是在优化错误的层。你今天就可以迁到 bun install 和 bun test,这是低风险、立刻见效的收益;至于要不要把整个运行时一起迁过去,应该等 profiler 真的告诉你值得再说。
Bun 和 Node.js 的性能,到底什么才重要
Bun 比 Node.js 快,这不是营销话术。无论是原始 HTTP 吞吐、启动时间,还是工具链速度,它都明显更好。但在真实世界的后端应用里,性能几乎从来不是卡在这些地方。它通常卡在数据库查询延迟、连接池被打满、请求处理器里的阻塞性 CPU 工作,以及藏在循环里的 N+1 查询。
如果这些才是你的瓶颈,那从 Node.js 换到 Bun 并不会让延迟明显下降。benchmark 不会把这件事告诉你,但这篇文章会。
Bun 真正加速的是什么
Bun 在一些非常具体的地方确实快得很明显。它的冷启动速度比 Node.js 更快;它基于 JavaScriptCore 和原生 API,文件和网络 I/O 的吞吐更强;bun install、bun test、bun build 这类工具也明显快于 npm、Jest 和 webpack;如果你的 HTTP handler 本身几乎不做什么工作,线上也确实更容易看到 benchmark 里的那些数字。
而这些 benchmark 背后的典型场景,其实很单纯:收到请求,几乎不做事,然后把响应发回去。在这种比赛里,Bun 会赢。对于 edge function、轻量代理或者静态文件服务,这就是正确选择。
但大多数生产后端,并不是长这个样子的。
Event Loop 到底是什么
Node.js 和 Bun 共享的是同一种并发模型:单线程,一次只做一件事。Event Loop 让这套模型变得可行,它的方式是:遇到 I/O 时,不阻塞在那里傻等,而是先把这段工作挂起来,等结果回来了再继续。
1 请求进来
2 → handler 开始执行
3 → 遇到 await db.query(...)
4 → Event Loop:先把这个挂起,我去做别的
5 → 数据库返回
6 → Event Loop:把这段继续捡起来跑
7 → handler 继续执行
8 → 响应发出这里最关键的词,是“委托”。Event Loop 不会让数据库查询本身变快,它只是自己不会傻坐在那里等。这个区别比你到底选了哪个运行时重要得多。
那个没人爱说的 Promise.all 问题
下面这种代码,很多有经验的开发者也会掉坑:
const [users, orders, inventory] = await Promise.all([
db.query('SELECT * FROM users WHERE active = true'),
db.query('SELECT * FROM orders WHERE status = pending'),
db.query('SELECT * FROM inventory WHERE stock > 0'),
]);表面上看,它是“并行”,很高效。但如果你的数据库连接池只有 10 个连接,而你同时扔进来 100 个查询,问题就来了。你以为自己在做并发,实际上你只是更快地把连接池打满。前 10 个先跑,后面的继续排队,等待中的任务还会继续吃内存和调度开销。一旦超时、重试、上游抖动叠到一起,系统只会更乱。
这就是为什么很多时候 Promise.all 看起来像性能优化,结果却更像一场拥堵制造机。
Event Loop 真正会在什么时候被堵住
只要你在请求处理流程里做阻塞性的 CPU 工作,Event Loop 就会卡住。不管你用的是 Node.js 还是 Bun,这件事都一样。
比如你在请求链路里做复杂 JSON 序列化、同步压缩、加密、图片处理,或者跑一段 CPU 很重的循环,都会让主线程在这段时间里完全无法接别的活。用户感受到的并不是“某一段代码慢”,而是整个服务突然开始发愣。
很多人一看到 Bun 的 benchmark,就会觉得运行时是性能故事的主角。但对真实系统来说,真正值得盯着看的,往往是 Event Loop 有没有被你自己的代码堵住。
benchmark 往往跳过的内存问题
Bun 跑在 JavaScriptCore 上,Node.js 跑在 V8 上。两者都很快,但垃圾回收策略不同,在高并发、长时间运行的场景下,这种差异会以 benchmark 不容易看出来的方式冒出来。
V8 的 GC 在 Google 那种超大规模环境里打磨了很多年,适合长期运行的服务进程,行为也更可预测。JavaScriptCore 的 GC 在浏览器、脚本、edge function 这种短生命周期负载里表现很好,但如果你把它拿来承受持续高并发服务负载,它有时会没那么稳定。
文章里给了一个非常典型的对比:Node.js 在高负载下通常会呈现一种比较可预期的锯齿形内存曲线,内存涨上去,GC 触发,再掉下来。Bun 在某些持续负载下,RSS 可能会在多次 GC 周期之间涨得更快,甚至有些 workload 里会出现 RSS 一路往上爬、不怎么掉下来的情况。你一重启,表面上又恢复正常;但过一阵子,又会重新出现。
这不是说 Bun 永远有这个问题,也不是说它改不好。文章也明确说了,这仍然是 Bun 正在持续改进的领域,而不是一个永恒缺陷。但如果你的应用会持有大量内存对象,要处理很重的 JSON,或者本来就贴着内存上限在跑,那你应该看的不是 p50 延迟,而是长时间维度上的 RSS 和 heap 变化。一个运行时如果快了 2 倍,但需要你每 6 小时重启一次,那并不能算升级。
所以 Bun 到底什么时候真的重要
如果你的工作负载是典型 CRUD API,真正的瓶颈通常还是数据库和网络 I/O,Bun 带来的提升通常只是边际性的。如果你做的是文件上传下载服务,I/O 吞吐本身就是核心瓶颈,那 Bun 就会有价值。edge function 或轻量代理也一样,这类场景本来就更吃原始 HTTP 性能,Bun 会很明显地赢。相反,如果你做的是 CPU 密集型处理,单线程模型本身才是问题所在,换到 Bun 也救不了你。至于开发工具链,Bun 的收益则非常直接:安装、构建、测试这些地方,它确实会快很多。
换句话说,Bun 不是一个“把慢应用修好”的工具。它更像是给那些本来就已经足够快、但还想在 I/O 层继续提速的应用再打一针强心剂。
在你切运行时之前
文章里给了一个非常务实的检查顺序。先问清楚你的 p99 延迟到底是从哪来的,别猜,先 profile。然后先去看数据库查询有没有用上索引,该跑 EXPLAIN ANALYZE 就先跑。再看连接池大小和并发请求规模之间是不是匹配。接着检查请求处理器里有没有同步 CPU 工作,该扔到 worker 的就扔。最后再看你是不是在循环里偷偷写了 N+1 查询。
先把这份清单走完。如果瓶颈最后真的落在原始 I/O 吞吐上,那 Bun 值得评估。否则,你只是在优化错误的层级。
迁移路径
bun install 和 bun test 今天就可以上,而且风险很低。开发体验上的收益是真实而且立刻能感受到的。至于要不要把整个生产服务直接跑在 Bun 上,那是另一回事。
先检查依赖。有些原生 Node.js 模块在 Bun 上还不能完全工作。你可以先直接跑 bun run index.ts,看看哪里会炸。兼容性在快速进步,但还没到百分之百。
如果是 greenfield 项目,Bun 已经开始像一个合理的默认选项了。如果是已有的 Node.js 应用,更稳的路线是:先迁工具链,再 benchmark 服务端,最后只有在数字真的证明有意义时,才把运行时一起迁过去。
结论
Bun 真的很快,这一点 benchmark 没骗人。但大多数应用真正需要的,是更少的查询、更聪明的缓存,以及不会被打爆的连接池,而不是一个更快的运行时。先把这些事情修好,再来讨论 Bun。
FAQ
Bun 比 Node.js 快吗?
是的。在原始 HTTP 吞吐、启动时间和工具链速度上,Bun 都更快。但在带数据库查询和连接池的真实后端应用里,这个差距往往没你想象中那么大。
我该从 Node.js 切到 Bun 吗?
只有在 profiler 告诉你,原始 I/O 吞吐真的是瓶颈时,才值得认真考虑。大多数应用真正受限的还是数据库延迟和 CPU 工作,不是运行时本身。
Bun 会让数据库变快吗?
不会。数据库延迟和 JavaScript 运行时是两件事。切到 Bun 不会让你的 SQL 突然飞起来。
到 2026 年,Bun 算 production-ready 吗?
对很多负载来说,算。但生态兼容性仍然有边角差异,迁移前一定要先验证自己的依赖。
Bun 最擅长的地方到底在哪?
edge function、轻量代理、静态文件服务,以及开发工具链。这些场景里,原始 I/O 和启动时间本来就是关键瓶颈。
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!