将 Node.js 项目迁移到 Bun:运行时内部机制、Lambda 函数及迁移流程
了解 Bun 在 Node.js 工具链中替代了什么、它为何能快速启动、如何在 AWS Lambda 上运行 TypeScript,以及如何逐步迁移项目。
来源: Everything You Need to Know About Bun: A Beginner’s Guide, Migration, and Architecture
一个典型的 Node.js 服务在运行任何业务逻辑代码之前,需要依赖四到五种不同的工具:包管理器、TypeScript 运行工具、测试框架以及打包工具。Bun 将这些工具整合为一个可执行文件,该文件同时承担运行时环境、包管理器、打包工具和测试运行器的功能,旨在成为 Node.js 的直接替代品。本指南将解释 Bun 实际上取代了哪些工具、它的速度优势来自何处、它是如何处理 TypeScript 以及无服务器部署的,还会介绍如何在不出现问题的情况下将现有项目迁移过来。如需包含 Deno 的更全面的基准测试对比,可参阅Node.js、Deno 与 Bun 的对比。
Bun 在典型工具链中取代了哪些工具
在浏览器外部运行 JavaScript 需要一个运行时环境,十多年来这一角色一直由 Node.js 扮演。不过,现代的 Node 项目通常会围绕其集成多个独立工具:
- 用于安装和管理包的 npm 或 Yarn;
- 用于编译或运行 TypeScript 的
tsc、ts-node或tsx; - 用于单元测试的 Jest 或 Vitest;
- 用于生成生产环境代码包的 esbuild、webpack 或 Rollup。
Bun 将所有这些功能整合到单个二进制文件中。安装它后,你无需额外配置即可获得运行时环境、包管理器、代码打包工具、测试运行器以及直接的 TypeScript 执行能力。其真正的好处在于省去了各种中间环节:配置文件更少,工具之间的版本冲突也更低。
与 Node.js 的优缺点对比
这一决策主要取决于速度与集成性,以及生态系统的发展成熟度。
Bun的优势所在
- 统一的工具链。无需配置独立的测试运行器、打包工具或TypeScript转译器。
- 快速的安装速度。Bun自身的测试显示,
bun install的速度比npm快多达30倍;实际速度还会因项目及缓存状态而有所不同。 - 迅速的冷启动性能。进程可在几毫秒内启动,这在无服务器环境中尤为重要,因为冷启动延迟会直接产生费用并影响用户体验。
- 无需额外配置即可使用TypeScript。
.ts和.tsx文件可直接运行,无需任何构建步骤或加载器脚本。
dotenv即可自动加载.env文件,且运行时内置了针对SQLite、Postgres、S3和Redis的客户端。Node.js仍具优势的方面
- 成熟度。Node.js拥有超过15年的实际应用历史,还有企业可依据的正式长期支持版本。而Bun较为年轻,仍在解决一些边缘案例中的兼容性问题。
- 原生插件支持。绝大多数npm包(通常超过95%)无需修改即可使用,但那些通过
node-gyp编译C++代码的旧版包仍可能出现问题。
为何 Bun 能快速启动和运行
并非仅仅因为“Zig 比 C++ 更快”
Bun 是用 Zig 编写的,人们很容易将它的快速性能归功于这种语言。但实际上,编译后的 Zig 与编译后的 C++ 在 CPU 上的运行速度大致相同。差异源于设计理念:Node.js 拥有多年积累的各种抽象层,而 Bun 则从零开始重新实现了文件 I/O、字符串处理和网络功能,并着重避免不必要的内存分配。
不同的 JavaScript 引擎
Node.js 运行在 V8 引擎上,这是谷歌为 Chrome 开发的引擎。而 Bun 则使用 JavaScriptCore,即苹果为 Safari 开发的引擎。JavaScriptCore 经过优化,具备快速的启动速度和强大的内存管理能力,这也解释了 Bun 在冷启动方面的优异表现。每种引擎在某些任务上都有优势,因此对于那些需要长时间运行且计算负荷较大的服务,应通过实际测试来评估其性能提升情况,而非直接假设会有改善。
优势不仅限于 macOS
尽管 JavaScriptCore 起源于 Safari,但它和 Bun 都已针对 x86_64、ARM64 架构的 Linux 以及 Windows 系统进行了原生编译。无论操作系统如何,这些性能特性都能在普通的云服务器上得到体现。
在 AWS Lambda 上运行 Bun
Bun 可以通过其内置的 Bun.serve() API 或者 Express、Fastify、Hono 等常见框架来提供 HTTP 后端服务。在 AWS Lambda 上,其快速的冷启动特性能够降低延迟和执行成本。由于 Lambda 不提供可在控制台选择的托管型 Bun 运行时,团队通常会采用以下三种方法之一:
- 自定义运行时层。将
bun二进制文件作为 Lambda 层发布,并在默认的 Linux 镜像provided.al2023上运行它。 - 容器镜像。基于官方的
oven/bunDocker 镜像构建函数,将其推送到 Amazon ECR,然后以容器形式部署。 - 用 Bun 构建,在 Node 上运行。使用
bun build将代码打包为单个.js文件,然后在 Lambda 的托管型 Node.js 运行时上执行该文件。
第三种方案最为保守:你既能享受 Bun 快速打包的优势,又能继续在生产环境中使用 AWS 支持的运行时环境。不过你将失去 Bun 专为运行时设计的 API 及冷启动优势,因此适合那些希望逐步采用 Bun 的团队。
Bun 如何执行 TypeScript
无论文件是 JavaScript 还是 TypeScript,都可以直接使用 bun 命令来运行:例如 bun index.js、bun index.ts,或者使用 bun --watch index.ts 实现文件更改时自动重启。
当运行 .ts 文件时,Bun 的转译器会即时移除类型注解,并且全部在内存中完成操作。它不会将 dist/ 文件夹、.build/ 目录或临时的 .js 文件写入磁盘。
重要的一点是,移除类型信息与检查类型信息并非同一回事。为保证启动速度,Bun 在运行时不会进行静态类型验证,因此存在类型错误的代码仍然可以运行。类型检查仍在你的编辑器中完成,比如 VS Code 或 Cursor,也可以在 CI 环境中通过 bunx tsc --noEmit 来执行。请务必将这一步视为必要操作。
逐步迁移现有的 Node.js 项目
由于 Bun 遵循 Node 的 API 和 npm 规范,普通项目通常只需进行少量修改即可完成迁移。建议在分支上操作,并在最后运行完整的测试套件。
第一步:移除旧模块和锁定文件
从干净的依赖树开始,这样Bun就能自行解析所有依赖:删除node_modules文件夹以及项目使用的锁定文件,无论是package-lock.json、yarn.lock还是pnpm-lock.yaml。
删除旧的锁定文件意味着会根据package.json中的版本范围重新解析依赖,因此你可能会使用到比之前更新的版本。如果需要完全一致的运行结果,请检查你的Bun版本是否支持直接迁移现有的锁定文件,并查看已解析版本的差异。
步骤2:使用Bun安装
运行bun install。Bun会读取你现有的package.json,下载相应的包,并生成一个bun.lock文件,该文件应当被提交到版本控制中。
步骤3:更新package.json中的脚本
请替换 scripts 部分中出现的 node、ts-node 或 tsx 相关内容:
- start:用
"bun src/index.js"替代"node src/index.js"; - dev:用
"bun --watch src/index.ts"替代"ts-node-dev src/index.ts"; - test:用
"bun test"替代"jest"或"vitest"。
该测试运行工具与 Jest 的 API 兼容性很高,但那些严重依赖 Jest 特有模拟功能或配置的测试可能需要调整。
第 4 步:更换 TypeScript 定义文件
对于 TypeScript 项目,需先运行 bun remove @types/node,再执行 bun add -d @types/bun,以此用 Bun 的类型定义替换 Node 的类型定义。
接着更新 tsconfig.json 中 compilerOptions 的 types 项。旧指南中此处写的是 "bun-types";而使用当前的 @types/bun 包时,该项通常为 "bun",因此请根据所安装的版本查阅 Bun TypeScript 的文档。
第5步:替换不兼容的模块
诸如 node:fs、node:path 和 node:http 这样的标准 Node 模块可以无需更改即可使用。那些依赖原生 C++ 绑定的包往往是问题的根源,不过有一些有简单的替代方案:
- 用
bcryptjs或@node-rs/bcrypt替换bcrypt; - 用 Bun 内置的
bun:sqlite替换sqlite3;
dotenv 调用,因为 Bun 会自动加载 .env 文件。对最后这一改动要谨慎处理。如果某些代码仍需在 Node 环境下运行,比如上述仅用于构建的 Lambda 设置中,应保留 dotenv 或显式加载配置,以避免不同运行时之间的行为差异。
核心要点
- Bun 的主要优势在于整合:一个二进制文件即可替代包管理器、TypeScript 运行工具、测试框架以及打包工具。
- 它的速度得益于注重内存分配的重新实现以及 JavaScriptCore 的快速启动机制,而非 Zig 本身比 C++ 更快。
- Bun 会在内存中移除 TypeScript 类型信息,但不会对其进行检查,因此请在 CI 流水线中保留
tsc --noEmit选项。 - 在 Lambda 环境中,你可以选择自定义层、容器镜像,或者使用 Bun 打包后在托管的 Node.js 环境中运行。
- 迁移过程主要是机械性的,但需注意锁文件重新解析、原生插件以及任何依赖自动加载
.env文件的行为。