首页 / 文章

将 Node.js 项目迁移到 Bun:运行时内部机制、Lambda 函数及迁移流程

了解 Bun 在 Node.js 工具链中替代了什么、它为何能快速启动、如何在 AWS Lambda 上运行 TypeScript,以及如何逐步迁移项目。

发布者:REACTAPP.TOP阅读约 6 分钟1493 词

来源: 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文件可直接运行,无需任何构建步骤或加载器脚本。
  • 内存占用更低。据报道,HTTP服务器的内存使用量通常比Node.js少25%到40%;在依赖这些数据之前,请先测试您自己的工作负载。
  • 内置电池支持功能。无需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/bun Docker 镜像构建函数,将其推送到 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 文件的行为。