试 Bun 之后我更关心的不是跑分,而是反馈链路有没有变短
Bun 刚开始被频繁讨论时,我也看了很多跑分。
安装依赖快、启动快、测试快、脚本执行快。数字确实漂亮,但让我对它感兴趣的不是某一次 benchmark,是日常开发里那条反馈链路。
前端开发有很多高频小动作:装依赖、跑脚本、跑一组测试、启动本地服务、执行内容校验。每次等几秒看起来不算什么,但一天里反复发生,就会影响写代码的节奏。
所以我后来试 Bun,不是想证明它能替代谁,而是想看它能不能让这些小循环更短。
我没有从主应用开始迁移
成熟项目里,工具链通常牵一发动全身。
一个 Next.js 或 Vite 项目里,可能混着框架插件、CSS 处理、环境变量、测试环境、CI 缓存、monorepo workspace、部署脚本。你只看本地能不能跑通,远远不够。
所以我更愿意从低风险脚本开始。
比如内容检查、数据生成、独立工具脚本:
1{ 2 "scripts": { 3 "check:content": "node scripts/check-content.js", 4 "generate:feed": "node scripts/generate-feed.js" 5 } 6}
可以先在分支里尝试:
1{ 2 "scripts": { 3 "check:content": "bun scripts/check-content.js", 4 "generate:feed": "bun scripts/generate-feed.js" 5 } 6}
这类迁移影响范围清楚。脚本输入输出稳定,失败影响范围小,对比也容易。能跑通就继续观察,遇到兼容问题也容易回退。
我第一个搬过去的是站点的 RSS/sitemap 生成脚本。改完命令之后第一反应是快了不少,原来 node scripts/generate-feed.js 冷启动要等小一秒,换 bun 之后基本是按下回车就出结果——省的不是 CPU 时间,是那种“等一下”的心理停顿。但真正让我留意的是一个细节:脚本里我用了 import { readFileSync } from 'node:fs',Bun 跑得很干净,可有一段老代码写的是 require('fs') 混着 ESM 的 import,在 Node 里靠 .mjs 和 package.json 的 type 勉强相安无事,到 Bun 里反而把这种含糊地带照出来了。这不算坑,反而是好事,逼我把模块系统理清楚。
还有一处值得记一笔。我那个脚本里用 new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) 格式化发布时间,本地 macOS 上 Node 和 Bun 输出一致,但 CI 的 Linux 镜像里 Bun 早期版本的 ICU 数据和 Node 不完全一样,生成出来的日期字符串差了个空格。要不是我做了文件 diff,这种东西根本发现不了。所以低风险脚本的“低风险”也是相对的——影响范围小,但语义差异该验证还得验证。
这比一上来把包管理、测试、构建、部署全换掉要稳很多。
Bun 的吸引力在“一体化”
Bun 不只是一个运行时。
它同时覆盖了:
- JavaScript 运行时。
- 包管理。
- 测试运行器。
- 打包器。
- TypeScript 和 JSX 支持。
这也是它和很多单点工具不一样的地方。过去前端项目常见组合是 Node 负责运行,pnpm/npm/yarn 负责安装,Vite/Webpack 负责构建,Jest/Vitest 负责测试,再加各种转译和配置。
每个工具都专业,但组合起来确实复杂。
Bun 的方向是把这些能力收束到一个入口里,减少工具之间的摩擦。对新项目、工具脚本、小型服务来说,这种体验很直接。
这种一体化在小脚本里体感最明显的,是它对 TypeScript 的态度。我写了个临时的内容统计脚本,直接 bun run stats.ts,不用配 ts-node、不用 tsx、不用 --loader,也不用专门起一个 tsconfig。Bun 把 .ts 当一等公民,开箱就转译。这在过去是要折腾半天的:装 ts-node 会和 ESM 打架,换 tsx 又要对齐 Node 版本,遇到 path alias 还得再叠一层 tsconfig-paths。Bun 把这一摞中间层直接抹掉了,对“写完即跑”的小工具特别友好。
它内置的几个 API 也很顺手。比如读文件我可以直接:
1const file = Bun.file("./posts/index.json"); 2const data = await file.json();
或者起一个极简的本地 mock 服务,连 express 都不用装:
1Bun.serve({ 2 port: 3001, 3 fetch(req) { 4 const url = new URL(req.url); 5 if (url.pathname === "/api/posts") { 6 return Response.json([{ id: 1, title: "hello" }]); 7 } 8 return new Response("not found", { status: 404 }); 9 }, 10});
这种“标准库自带电池”的感觉,是我愿意在边缘场景先用它的主要原因。
但一体化也意味着你要接受它的约束。这些好用的 Bun.* API 一旦写进脚本,就是绑定关系,哪天想退回 Node 得手动还原。项目越成熟,既有工具链越稳定,迁移收益就越需要算清楚。
快不快要放到具体链路里看
我现在不太喜欢只讨论“Bun 比某某快多少”。
更实际的问题是:它快的地方是不是你项目真正痛的地方?
如果团队每天痛的是依赖安装慢、脚本启动慢、测试反馈慢,那 Bun 可能很值得试。
如果真正慢的是接口、构建产物过大、类型检查太重、E2E 环境启动慢,那换 Bun 未必能直接解决。
我会把反馈链路拆开看:
1安装依赖 -> 启动开发服务 -> 修改代码 -> 跑测试 -> 构建 -> 部署
然后问:哪一段最耗时间?哪一段最影响开发节奏?Bun 能不能安全替换这一段?
拿我自己的项目举例。我真量过一次,用最朴素的办法——删掉 node_modules 和 lockfile,分别跑安装:
1rm -rf node_modules package-lock.json 2time npm install 3# 然后清干净再来一遍 4rm -rf node_modules bun.lock 5time bun install
冷装(没有任何缓存)的差距没有跑分页那么夸张,但 bun install 确实快出一截,热装(有全局缓存、只是重建 node_modules)差距更明显,因为 Bun 用的是 hardlink/clone 那套,几乎不重复拷贝文件。对一个一天要切好几个分支、动不动 install 一次的项目,这一段省下来的时间是实打实的。
但同样这套测下来,我发现自己项目真正的瓶颈根本不在安装,而在类型检查和 Next 的构建。tsc --noEmit 慢、next build 慢,这俩跟你用不用 Bun 装依赖没半点关系。所以那次量完我反而冷静了:Bun 能治的是我的“快病”,治不了我的“慢病”。
这样比泛泛追跑分更有意义。
兼容性要实际验证
Bun 发展很快,但工具链迁移不能只看宣传页。
我会重点验证几类问题:
- Node API 行为是否一致。
- 依赖是否使用原生扩展。
- lockfile 和包管理策略是否统一。
- CI 环境是否稳定。
- 测试里的 mock、fake timer、snapshot 是否一致。
- 构建产物和原流程是否等价。
最容易被忽略的是“能跑”和“语义一致”不是一回事。
比如测试迁移到 bun test,跑得快当然好,但要确认断言行为、mock 机制、环境变量、DOM 环境都符合预期。速度提升不能以测试含义变化为代价。
我在测试这一层踩过最实在的一个坑:bun test 不是 Jest 的 drop-in 替代。它确实兼容了很大一部分 expect 语法,但具体到 jest.mock() 的提升行为、jest.useFakeTimers() 的实现、还有自定义 matcher,差异是存在的。我有一组依赖 vi.mock/jest.mock 自动 hoisting 的测试,搬过去之后 mock 没生效,排查半天才反应过来不是我代码的问题,是运行器语义不一样。更别说要测 React 组件,bun test 默认没有 jsdom,得自己配 happy-dom 或者 --preload 注入环境:
1// happydom.ts 2import { GlobalRegistrator } from "@happy-dom/global-registrator"; 3GlobalRegistrator.register();
1# bunfig.toml 2[test] 3preload = ["./happydom.ts"]
这事我的结论是:纯逻辑的、不碰 DOM、不重度用 mock 的单测,搬去 bun test 收益明显且风险低;一旦测试里堆了大量 Jest 生态特有的东西,迁移就不是“换个命令”那么简单了,老实留在 Vitest/Jest 反而省心。
工具脚本也是一样。生成出来的文件要和原来对比,而不是只看命令退出码。我的土办法就是迁移前后各跑一遍,把产物 git diff 一下,一个字节都不差才算过——前面那个日期空格的问题,就是这么逮到的。
我会用 checklist 推进
如果一个成熟项目要试 Bun,我会按这种顺序来:
11. 单独开分支,不影响主流程 22. 先迁移一个低风险 script 33. 对比迁移前后的输出文件 44. 在 CI 里并行验证,不立刻替换主流程 55. 连续几轮无差异,再扩大到更多脚本 66. 最后才考虑测试、包管理或构建
这个流程看起来慢,但工具链最怕的是“本地快了,线上炸了”。
第 4 步在 CI 里并行验证,我具体是这么干的:不动原来的 job,新加一个只跑 Bun 的影子 job,让两边跑同样的脚本,结果都上传成 artifact,再比对。大概长这样:
1jobs: 2 build-node: 3 runs-on: ubuntu-latest 4 steps: 5 - uses: actions/checkout@v4 6 - uses: actions/setup-node@v4 7 with: { node-version: 20 } 8 - run: npm ci 9 - run: npm run generate:feed 10 - uses: actions/upload-artifact@v4 11 with: { name: feed-node, path: public/feed.xml } 12 13 build-bun: 14 runs-on: ubuntu-latest 15 steps: 16 - uses: actions/checkout@v4 17 - uses: oven-sh/setup-bun@v2 18 - run: bun install --frozen-lockfile 19 - run: bun run generate:feed 20 - uses: actions/upload-artifact@v4 21 with: { name: feed-bun, path: public/feed.xml }
两个 artifact 拉下来 diff 一下,连着几次提交都零差异,我才敢把主 job 换掉。这里要提醒一句:Bun 1.2 起默认 lockfile 是文本格式的 bun.lock,和 package-lock.json/pnpm-lock.yaml 一样能直接 git diff 看出改动,可读性好很多(更早版本用的是二进制的 bun.lockb,那种格式没法直接 review)。就算换成了文本格式,它和 package-lock.json/pnpm-lock.yaml 也不是一回事,过渡期容易出现两份 lockfile 并存、版本各算各的情况。CI 上一定要带 --frozen-lockfile,否则它可能在 CI 里偷偷解析出和本地不同的依赖版本,这种偏差最难查。
工程化不是追求最短迁移路径,而是追求可验证、可回退、可解释。
它适合先用在哪些地方
我觉得 Bun 很适合从这些地方试:
- 独立内容生成脚本。
- 本地 mock 服务。
- 小型内部工具。
- 低风险测试包。
- 新项目原型。
- monorepo 里的边缘工具包。
不太建议第一步就动这些地方:
- 主应用构建链路。
- 复杂 CI/CD。
- 依赖很多原生模块的服务。
- 已经非常稳定的 pnpm workspace。
- 强依赖特定测试框架语义的项目。
这不是说不能迁,而是先后顺序要讲成本。工具链迁移如果没有明确收益,很容易变成“为了新而新”。
我对工具链趋势的感受
这两年前端工具链有一个明显方向:反馈要更快,默认配置要更少,工具之间的分工在重新收敛。
Bun 是这个方向里很有代表性的一个选择。它提醒我,开发体验不是小事。一次脚本快几秒,单看不起眼,但高频反馈链路变短,团队写代码、跑验证、改问题的节奏都会变好。
但工具选型最后看的不是兴奋感,而是净收益。
如果一个团队已经有稳定的 Node + pnpm + Vitest + Vite 流程,Bun 要带来的不仅是速度,还必须是不增加维护负担的速度。
这里有个容易被低估的隐性成本:团队认知。我在脚本里用了 Bun.file、Bun.serve 之后,新来的同事如果只熟 Node,看到这些 API 会先愣一下。工具越“魔法”,门槛迁移得越隐蔽——不是装不上,是看不懂、改不动、出了问题不知道去哪查文档。所以哪怕只在边缘脚本里用,我也会在 README 里写清楚“这个脚本依赖 Bun,原因是 X,回退方案是 Y”。把绑定关系写在明处,比让它在某次半夜的构建里突然冒出来强。
另一个我现在的折中做法是:能用 Web 标准 API 就尽量用 Web 标准 API,少用 Bun.* 专属语法。比如 fetch、URL、Response 这些 Node 18+ 和 Bun 都支持,写出来的脚本两边都能跑,迁移和回退的成本都低。把“用了 Bun 的速度”和“被 Bun 锁死”这两件事分开看,是我这段时间最大的体会。
我现在看 Bun,不会只看它能不能“替代 Node”。
对我来说更实际的问题是:它能不能让某一段开发反馈更短,并且迁移成本、兼容风险、团队理解成本都可控。
如果答案是肯定的,就从小脚本开始试。如果答案还不清楚,就继续观察。工具链更新很快,但项目稳定性更贵。