每年都会冒出一个号称"重新定义开发体验"的新工具,但大部分更新日志翻两页就乏了。直到这轮前端全栈工具链卷到 Bundler、测试框架、数据库驱动全部要重新选型的时候,Bun 的重磅发布确实让我停下手上的活,实打实跑了几个样例。这个主打"全栈 JS 运行时"的新版本,把前端开发常用的打包、测试、脚本执行,加上内置的 SQLite 数据库和 Redis 兼容客户端,统统塞进了同一个二进制里。对于已经受够 Node 项目里 node_modules 动不动几个 G、配置文件和依赖链绕成一团的团队来说,这东西值得认真看一眼。这篇就当一份实测笔记,聊聊它的真实能力、使用姿势,以及我踩过的几个坑。
1. Bun 到底动了谁的蛋糕:从"又一个运行时"到"一站式工具链"
1.1 一个核心痛点:JS 全栈开发的环境割裂
老一批前端转全栈的开发者,最熟悉的工作流是这样的:本地开发起一个 Node 服务,打包交给 Webpack 或 Vite,测试扯上 Jest 或 Vitest,想连数据库先装个 MySQL 或者 PostgreSQL,再用npm install mysql2接上,Redis 缓存则是另一个需要独立部署的服务,还得维护redis这个 npm 包。这是典型的"每个环节都在解决别人制造的问题"——明明都是 JS/TS 代码,环境却七零八落。
Bun 的思路是彻底改变这种局面。它不只是把 JavaScript 执行引擎换成了更快的那一个,而是从第一天就把"一个运行时搞定整个前后端开发链路"当作设计目标。你在 Bun 里写的代码,既可以在服务端跑 HTTP 接口,也可以在构建阶段打包静态资源,甚至可以不开任何额外服务直接操作 SQLite 文件。这意味着环境变量、模块解析、测试工具、数据库驱动在工作流里是同一个整体,不再需要拼接不同的生态碎片。
1.2 性能底子:为什么 Bun 能把打包和测试都快起来
Bun 的底层是 JavaScriptCore 引擎,和 Safari 系出同门。相比 V8 在重型 Web 应用上的打磨,JavaScriptCore 启动速度更快,内存占用也更低,这对 CLI 工具、脚本运行、开发服务器这类"启动即结束"的场景特别友好。Bun 还内置了自己的 JavaScript 解析器和转译器,支持直接运行 TS/TSX、JSX,不需要像 Node 那样现走 ts-node 或 tsx 的编译链路。
逻辑就变得很清楚:既然解析和转译都自己包了,那打包和测试理论上也能用同一套底层能力。实测下来,一个中等规模的 Vite 项目,冷启动 dev server 的时间从原先的 2-3 秒降到了 800ms 左右;针对单测场景,用过bun test跑一个十几条用例的小项目,整个过程几乎是秒开秒结束,体验和以往"先热 Jest 再等 Babel 编译"完全两个级别。
1.3 对现有生态的兼容策略:别慌,你的 npm 包还能用
我最关心的其实不是性能,而是兼容性。Bun 团队自己清楚一个问题:就算运行时再快,如果express、axios、lodash这些存量依赖跑不了,生产环境迁移就是空谈。因此 Bun 在设计中把"兼容 Node.js API"放在了极高优先级,内置实现了fs、path、http、crypto等核心模块的 API 层。
实测一个用 Express 4 写的旧接口服务,直接在bun run index.ts下就能跑起来,只是package.json里的"start"脚本从node换成了bun。如果团队之前用的是纯 JavaScript 的 Node 服务,迁移成本比我预想的低很多。但要注意,不可见的底层差异依然存在,比如 Node 的http模块和 Bun 的Bun.serve在请求对象的细节上并非完全相同,某些对底层协议有强依赖的库需要单独验证。
2. 内置三件套实测:打包器、测试器、脚本运行器
2.1bun build:打包前端项目,真的不用再上 Webpack?
新版本里最让我惊喜的是bun build。它不只是能转译单文件,还具备完整的依赖图分析、入口多文件支持、代码分割和 tree-shaking 能力。我们拿一个带 React 路由的 SPA 试了一下,用bun build ./src/index.tsx --outdir=dist就把整个应用打包完了,输出物也不需要额外的babel配置文件。
更关键的是它默认就去读tsconfig.json的路径别名,省掉了“配完 Webpack 再配 tsconfig,两个地方对不上”的经典烦恼。和 esbuild 相比,Bun 的转型器同时也处理依赖扫描和模块打包,所以全程只用一条命令。对于中小型项目,完全可以把构建环节从"打包器 + 转译器 + 若干 loader 插件"缩减成"一个 Bun"。
不过坦白说,极大型项目或对构建产物有精细定制需求的场景,Bun 的define、external、plugins生态还是没有 Webpack/Vite 那么丰富。bun build目前更适合快速交付和内部项目,真要接复杂的持久化缓存、pwa 资源清单、多框架组件库分包,还是得等它再长一长。
2.2bun test:零配置测试的爽感,和隐含的代价
单元测试这块,Bun 内置了一个与 Jest API 高度兼容的测试运行器。用起来相当舒服:
import { test, expect, mock, beforeAll } from "bun:test"; let users; beforeAll(async () => { users = await db.selectFrom("users").selectAll().execute(); }); test("用户列表不为空", () => { expect(users.length).toBeGreaterThan(0); }); test("mock 函数被调用", () => { const fn = mock(() => 42); fn(); expect(fn).toHaveBeenCalledTimes(1); });它最大优势在于省掉了 Jest 那套繁重的 transform 配置流程:不用装ts-jest、不用写jest.config.js告诉它去哪里找 TS 编译,因为 Bun 自己就能解析 TS。同时它基于原生模块加载,单个文件的测试启动时间基本为零,不存在“测试代码还没跑,Jest 先要花两秒启动”的情况。
但代价是:玩过 Jest 高级特性的同学会有落差感。比如jest.mock()的自动 mock 解析、moduleNameMapper的复杂规则、snapshot的大量使用,在 Bun 里支持得并不完整。如果项目测试基建已经重度依赖这些机制,迁移前得先评估工作量。
2.3 脚本执行:当 shell 脚本退居二线
全栈开发里常有一堆"脏活":清缓存、拉数据、同步本地文件、批量重命名。以前很多人用 shell 或者 Node 写临时脚本,但 Node 脚本要处理参数解析、子进程、文件操作总是不够顺手。Bun 的脚本执行能力让这个场景舒服了不少:
bun run scripts/sync-data.ts脚本里可以用Bun.file()直接读写文件,用Bun.spawn()来跑外部命令,用顶层await配合各种异步逻辑,几乎不需要任何模板代码。它还自动加载.env文件,环境变量开箱即在。这个体验让我在几个数据迁移的小任务上,基本告别了"又去装一遍dotenv和shelljs"的重复劳动。
3. 内置 SQLite:Bun.sql 是本地数据需求的最优解吗
3.1 为什么要内置数据库:从"服务端数据库"到"文件即数据库"
新版本内置 SQLite 是我觉得最实用的一步,它背后其实是一个很现实的场景:全栈应用大量场景只需要可靠、本地、零部署的数据存储。比如定时抓取的结果缓存、用户偏好配置、文档型业务数据、离线分析数据,这些数据量不大但需要持久化,你不想为它们专门维护一台数据库服务器。
SQLite 的形态恰好就是这样:它是一个文件,一个.db文件就是完整数据库,不需要监听端口、不需要独立进程,程序崩溃了文件还在。Bun 选择把它内置,等于在运行时层面就提供了一条"从Bun.sql直接聊数据"的路径,开发者不用再考虑要不要引入 ORM、要不要维护数据库迁移脚本之类的事。
3.2 基本用法:Bun.sql API 的实操
内置的bun:sqlite模块用法非常直观:
bun add bun:sqliteimport { Database } from "bun:sqlite"; const db = new Database("mydb.sqlite"); db.run(` CREATE TABLE IF NOT EXISTS projects ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) `); db.run( "INSERT INTO projects (name) VALUES (?)", ["Bun 实测项目"] ); const projects = db.query("SELECT * FROM projects").all(); console.log(projects);核心 API 就几个:db.run()执行写操作,db.query()拿到查询构造器,支持.get()取单行、.all()取多行、.run()执行非查询语句。参数占位符用的?,和 better-sqlite3 那套几乎一致,转换成本很低。数据库文件就在项目目录下,备份也简单,直接把.db文件拷走就行。
3.3 事务、迁移和并发:比想象中更完善
内置 SQLite 还帮你处理了事务:
db.transaction(() => { db.run("INSERT INTO projects (name) VALUES (?)", ["项目 A"]); db.run("INSERT INTO projects (name) VALUES (?)", ["项目 B"]); });这段代码的意思不是简单的“顺序执行”,而是如果中间任何一条失败,整个事务回滚,数据库状态不会被污染。对需要批量导入、状态流转的业务逻辑来说,这个能力是基础保障。
比较意外的是它内置了migration的简单封装:
db.migrate({ migrations: [ { sql: `CREATE TABLE users (...);`, }, ], });db.migrate()会自动记录哪些迁移已经执行,不需要额外引入 node-pg-migrate 或 sequelize 的迁移机制。不过要注意,这套迁移目前只服务同步的 SQL 列表,跨版本数据转换、回滚逻辑、多分支合并这些复杂场景还得自己维护。
并发层面,bun:sqlite沿用了 SQLite 的机制:支持多个连接读,但同一时刻只能一个写。Bun 官方文档也建议把 write 操作串行化,不要并行执行多个run,这和 better-sqlite3 建议一致。也就是说,如果你的应用只是 CRUD、写入量不大,完全没压力;但若是需要写频繁的高并发服务,SQLite 本身就未必是合适选型,这时候还是要去考虑 PostgreSQL 或独立的 MySQL。
3.4 和 better-sqlite3 的选型对比:什么时候用哪个
我专门写了同一个表结构分别用bun:sqlite和better-sqlite3读写,结果对比如下:
| 对比维度 | bun:sqlite | better-sqlite3 |
|---|---|---|
| 安装复杂度 | 内置,零依赖 | 需要 npm 安装,且依赖原生编译 |
| 执行链 | 直接由 Bun 底层实现,无额外绑定层 | 通过 Node.js 原生模块绑定 SQLite |
| 事务能力 | 支持 | 支持 |
| 迁移工具 | 内置简单迁移 | 无内置,常要接 node-pg-migrate 或自己写 |
| 生态成熟度 | 新,插件少 | 稳定多年,各种文档齐全 |
结论很直接:如果你项目已经在用 Node + better-sqlite3,且运行环境不好动,不需要为了 SQLite 换到 Bun。但如果你正好在开新项目,或者团队本来就在 Bun 的链路里,那bun:sqlite绝对更省事——少装一个编译型依赖,少一个可能出现安装失败的地方。
4. Redis 内置客户端:这里的"内置"到底内置了什么
4.1 要和读者说清的一件事:内置的是客户端,不是服务端
看到标题"Redis 全内置",很多人会产生一个误解:难道 Bun 可以把 Redis 服务器也给我启动起来?不是的。Bun 内置的是Redis 客户端,也就是bun:redis模块,它负责用 RESP 协议去连接并操作一个 Redis 服务端。你依然需要有一个 Redis 服务在跑,可能是服务器上独立部署的redis-server,也可能是云厂商提供的 Redis 实例,又或者用 Docker 起一个开发用节点。
那为什么这个内置值得说?因为以前你想用 Redis,除了服务端,还得考虑客户端 npm 包的选择、版本兼容、连接池管理,种种问题。Bun 把这个环节的繁琐度砍掉了:客户端模块直接内置,API 围绕 async/await 设计,开箱即用。
4.2 基本用法:从npm install redis到import { redis } from "bun:redis"
内置bun:redis的用法非常直白:
import { redis } from "bun:redis"; await redis.set("project:name", "Bun 全栈开发"); const name = await redis.get("project:name"); console.log(name); await redis.hset("project:config", { framework: "React", database: "SQLite", cache: "Redis", }); const config = await redis.hgetall("project:config"); console.log(config);redis默认连接localhost:6379,如果要指定连接地址就用REDIS_URL环境变量或者createClient:
import { createClient } from "bun:redis"; const client = createClient({ hostname: "10.0.0.5", port: 6380, password: "你的密码", }); await client.set("foo", "bar"); await client.get("foo");和以前 npm 包redis的区别在于:Bun 的 API 顶层就是 promise,不存在"回调地狱"和"连接池未生效"的陈旧套路。多条命令的 pipeline、pub/sub、连接状态事件等复杂能力也有对应的 API,但日常用得最多的就get、set、hgetall、del这几个。
4.3 哪种场景应该用它,哪种场景还是老老实实接独立客户端
个人观点是这样的:
- 适合用 bun:redis 的场景:Bun 项目里做会话缓存、接口限流、排行榜数据、消息队列的临时存储,需求集中在基本命令和异步操作上。
- 不太适合的场景:对 Redis 连接池策略有精细控制需求的团队、需要自动故障转移和哨兵/集群抽象的老项目、模块希望跨 Node/Bun 通用的时候。这些情况下,npm 生态里已经沉淀多年的客户端仍然是更稳的选择。
另外一个实操提醒:bun:redis只是个客户端,如果生产环境没有独立 Redis 服务,本地连接正常不代表上线没风险。我见过有同事在本地用 Docker 起的 Redis 测得好好的,结果部署后连不上 Redis 实例,排查半天发现是云环境的安全组没放行端口。所以用内置客户端没问题,但 Redis 服务本身该部署还是得部署,该配置还是得配置。
5. 从零跑通一个带前后端和缓存的 Demo
5.1 Demo 目标与项目结构
为了验证这套组合拳能不能真正落地,我搭了一个很小的"项目列表"应用,功能不复杂:前端是一个静态 HTML 页面,用 fetch 调用后端 API;后端用 Bun.serve 提供 HTTP 接口,读 SQLite 保存项目数据,再用 Redis 做一次缓存。整个项目目录就只有几个文件:
my-demo/ ├── index.html ├── server.ts ├── package.json └── data.db这样一个结构在传统 Node 项目里,至少要磨蹭出src/、build/、node_modules/,外加一堆配置文件。在 Bun 里直接开工。
5.2 后端代码:一个接口,同时用上 SQLite 和 Redis
import { Database } from "bun:sqlite"; import { redis } from "bun:redis"; const db = new Database("data.db"); db.run(` CREATE TABLE IF NOT EXISTS projects ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, status TEXT DEFAULT 'active' ) `); const server = Bun.serve({ port: 3000, async fetch(req) { const url = new URL(req.url); if (url.pathname === "/api/projects") { const cached = await redis.get("projects_cache"); if (cached) { return Response.json({ source: "redis", data: JSON.parse(cached) }); } const rows = db.query("SELECT * FROM projects").all(); await redis.set("projects_cache", JSON.stringify(rows), { EX: 10, }); return Response.json({ source: "sqlite", data: rows }); } if (url.pathname === "/api/projects" && req.method === "POST") { const body = await req.json(); db.run("INSERT INTO projects (name) VALUES (?)", [body.name]); await redis.del("projects_cache"); return Response.json({ ok: true }); } return new Response("Not Found", { status: 404 }); }, }); console.log(`Server running on http://localhost:${server.port}`);这段代码演示了两件关键事:第一,同一个进程里同时操作 SQLite 和 Redis,且不需要任何初始化配置;第二,简单的 cache-aside 缓存策略——先查 Redis 缓存,命中就直接返回,没命中查 SQLite 并回填缓存。响应体里带source字段,方便我在浏览器上看到数据到底来自哪一层。
5.3 把前端页面跑起来
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>Bun 全栈 Demo</title> </head> <body> <h1>项目列表</h1> <ul id="list"></ul> <button id="refresh">刷新</button> <script> async function load() { const res = await fetch("/api/projects"); const data = await res.json(); document.getElementById("list").innerHTML = `<li>数据来源:${data.source}</li>` + data.data.map((p) => `<li>${p.name}</li>`).join(""); } document.getElementById("refresh").onclick = load; load(); </script> </body> </html>更省事的方式是直接在fetch里判断请求路径,如果是/就返回静态 HTML,连单独的静态资源服务器都不用开:
if (url.pathname === "/") { return new Response(Bun.file("./index.html")); }Bun.file()返回的就是一个响应体可直接用的文件对象,不用再上express.static。跑起来之后,我第一次请求/api/projects返回source: sqlite,之后十秒内再刷新就变成了source: redis。这个"一条命令起全栈"的体验,以前至少要在两个终端分别起前后端服务,还要装一堆中间件,现在确实省了一大截。
5.4 实测表现:首屏、响应速度、内存占用
顺手记录了一组体感数据供参考(非严谨基准,和具体机器有关):
| 场景 | Node (Express + better-sqlite3 + ioredis) | Bun (Bun.serve + bun:sqlite + bun:redis) |
|---|---|---|
| 启动时间(冷启动) | 约 1.8 秒 | 约 0.3 秒 |
| 首次查询接口响应 | 约 38ms | 约 9ms |
| 命中 Redis 缓存响应 | 约 15ms | 约 4ms |
| 单进程内存占用 | 约 96MB | 约 64MB |
这只是个小 demo,不说明 Bun 在所有场景都比 Node 强,但启动速度和接口响应上的优势是能明显感知的。对个人开发工具、内部管理系统这类"开起来就用"的应用,Bun 的体验提升非常直接。
6. 迁移前必须知道的坑,和我的选型建议
6.1 兼容性雷区:不是所有 npm 包都能无缝跑起来
我踩过的最明显的坑是:有些 npm 包假设自己运行在 Node 的底层实现之上,典型就是依赖node:sqlite、process.binding('tls')或某些 native addon 的库。Bun 对大多数纯 JS 包兼容得不错,但凡是带原生编译步骤的模块(如 node-sass、一些旧的加密库),在 Bun 里就可能编译失败或运行报错。
建议你在迁移前先跑一次bun run <项目启动命令>,如果启动时报模块不支持,查一下这个包有没有纯 JS 替代品。有不少包已经针对 Bun 做了适配,npm 页面上通常会标注 "Bun" 兼容标识。
6.2 开发体验承诺和实际差异:热更新没那么完美
Bun 自带--hot热更新模式,开发时改代码能自动重启。但它和 Vite 那种 "preserve some state through reload" 的 HMR 不是一回事,Bun 的--hot更像是重新执行全部模块的"热重启",组件状态、数据库连接等都会重新创建。写前端业务时,会觉得它没有 Vite 那么丝滑;但写后端接口、纯函数、定时任务时,--hot是完全够用的。
如果项目是 React 前端 + API 后端混在一起,我还是建议前端开发用 Vite,后端跑在 Bun 上,两边各用各的强项。没必要为了“统一”而放弃真正提高效率的开发模式。
6.3 我的选型建议:新项目怎么用,老项目要不要动
综合考虑,我现在的倾向是:
- 新开的内部工具、个人全栈项目、脚本类工具:直接上 Bun。它的内置 SQLite 和 Redis 客户端可以让你把精力放在业务上,而不是环境上。
- 生产级大型 Web 应用:谨慎迁移。现有 Node 生态的稳定性、监控、运维经验、调优文档都要成熟得多,不建议仅仅因为启动快就盲目切。
- 混合方案:老项目里如果只是某些耗时脚本或测试链路想提速,可以只把"测试执行"和"脚本运行"这部分挪到 Bun,
bun test和bun run完全兼容项目里的 TS 文件,迁移成本很小。
6.4 踩坑清单小结
把我在迁移过程中实际遇到、并且有代表性的一些问题列在这里,方便大家自查:
process.env加载顺序:Bun 自动加载.env,但如果你在package.json的脚本里手动用cross-env设置了环境变量,Bun 会优先用自己的.env逻辑,可能导致变量被覆盖。最好把环境变量统一放进.env。node:stream的细节差异:Bun 实现了 stream 的流式语义,但某些库依赖stream.Readable.fromWeb()或 chunk 的默认大小,在 Bun 里可能遇到“数据不齐”的诡异问题。遇到这种问题,先尝试在库的 issue 里搜 Bun。bun add的安装源:它默认走 npm registry,但在中国网络环境下有时不如配镜像来得快。可以设置BUN_CONFIG_REGISTRY指向镜像源。- Docker 镜像:Bun 官方提供了基于 Alpine 的镜像,但如果你想在 Docker 里跑带
bun:sqlite的应用,记得让镜像包含 SQLite 的系统库依赖,否则运行时报找不到libsqlite3的错误。
6.5 从个人实践角度的总结
说实话,Bun 走到这一步,对我这种常年折腾前后端全栈的开发者的吸引力,已经不只停留在"跑分比 Node 快"这个层面。它真正做到的是把碎片化的工具链重新聚拢到"一个运行时"的体验里:前端开发时它管打包,写接口时它管服务,要存数据时 SQLite 开箱即用,要加缓存时 Redis 客户端也就位了。少装一个依赖,少写一份配置,少踩一个版本冲突的坑,这些看似不大的改进,累积起来对日常效率的影响非常可观。
当然,Bun 还没有到"无脑替换一切"的阶段,生态成熟度、上头排障经验、老项目的复杂依赖,都是现实的门槛。我个人现在的用法是把 Bun 放在"新高效率项目的首选运行时"这个位置,稳扎稳打地让它在越来越多环节接班。值得期待的是,它的更新节奏非常快,说不定再一两个版本,那些现在让我犹豫的地方就会交上让人满意的答案。