本文详细记录了 Trigger.dev 团队将关键服务 Firestarter 从 Node.js 迁移至 Bun 的全过程,通过四轮性能优化(移除 SQLite、迁移 HTTP 栈、精简热路径、编译为二进制)和修复一个 Bun 特有的内存泄漏问题,最终实现了吞吐量提升 5 倍、延迟大幅降低、内存占用减少的显著成果。
📝 详细摘要
文章分享了 Trigger.dev 团队对其热启动连接调度服务 Firestarter 进行深度性能优化的完整案例。该服务最初基于 Node.js 构建,存在 CPU 占用过高的问题。团队通过四轮系统性优化:首先用 Map 替换过度设计的 SQLite 查询,使吞吐量翻倍;接着将 HTTP 服务器从 Node.js 迁移至 Bun 的原生 Bun.serve(),再次实现性能翻倍;然后通过 CPU 性能分析,识别并移除了 Zod 解析、请求头转换和日志序列化等热路径开销;最后利用 bun build --compile 将服务编译为单一二进制文件,进一步提升了性能并减小了镜像体积。文章还重点剖析了在迁移过程中发现的一个 Bun 特有的内存泄漏陷阱——未 resolve 的 Promise 会持续占用内存,并给出了修复方案。整个过程附有详尽的基准测试数据、生产环境监控图表和可复现的调试方法,为从 Node.js 迁移至 Bun 提供了极具价值的实战参考。
💡 主要观点
-
性能优化必须始于精准的性能分析,而非盲目猜测。
团队通过 node --prof 和 Bun 的 --cpu-prof-md 等工具,准确识别出 SQLite 查询、HTTP 栈开销、Zod 解析等真正的性能瓶颈,从而进行针对性优化,避免了无效劳动。
fetch handler 中,每个 Promise 都必须被 resolve 或 reject,否则会导致请求上下文内存泄漏。这与 Node.js 中响应绑定在 socket 上的自动清理机制不同,是迁移长轮询或流式端点时的关键陷阱。
bun build --compile 能为长期运行的服务带来显著的性能与部署收益。
将服务编译为单一可执行文件,在零代码改动的情况下,为团队带来了 14% 的吞吐量提升和 24% 的 p95 延迟改善,同时将容器镜像大小从约 120MB 缩减至 68MB,简化了部署。
idleTimeout 配置不当等仅在真实负载下才会暴露的问题。
💬 文章金句
- 我们在一项对延迟极为敏感的服务中,用 Bun 替换了 Node.js,吞吐量直接提升了 5 倍。
- 在 Bun 中,fetch handler 的契约有所不同。每个 Promise 都必须被 settle(这不是建议,而是硬性要求)。
- 先做性能分析,再谈优化。移除 SQLite 事后看来显而易见(事后诸葛亮不都这样吗?),但我们之所以能发现它,纯粹是因为做了性能分析。
- 修复内存泄漏后,CPU 上升了约 5%。这就是正确清理那些此前被悄悄泄漏的连接所需付出的成本。
- Bun 的 HTTP 模型与 Node 有本质区别。响应的生命周期绑定在 Promise 上,而非 socket 上。
📊 文章信息
AI 初评:89
来源:前端早读课
作者:前端早读课
分类:软件编程
语言:中文
阅读时间:25 分钟
字数:6015
标签: Bun, Node.js, 性能优化, 内存泄漏, 后端架构