ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Bun实测:一个运行时搞定全栈开发,内置打包测试SQLite与Redis

Bun实测:一个运行时搞定全栈开发,内置打包测试SQLite与Redis 每年都会冒出一个号称重新定义开发体验的新工具但大部分更新日志翻两页就乏了。直到这轮前端全栈工具链卷到 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 --outdirdist就把整个应用打包完了输出物也不需要额外的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. 内置 SQLiteBun.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:sqlitebetter-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环境变量或者createClientimport { 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. 从零跑通一个带前后端和缓存的 Demo5.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 和 Redisimport { 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 langzh-CN head meta charsetUTF-8 / titleBun 全栈 Demo/title /head body h1项目列表/h1 ul idlist/ul button idrefresh刷新/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 生态的稳定性、监控、运维经验、调优文档都要成熟得多不建议仅仅因为启动快就盲目切。混合方案老项目里如果只是某些耗时脚本或测试链路想提速可以只把测试执行和脚本运行这部分挪到 Bunbun 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 放在新高效率项目的首选运行时这个位置稳扎稳打地让它在越来越多环节接班。值得期待的是它的更新节奏非常快说不定再一两个版本那些现在让我犹豫的地方就会交上让人满意的答案。
返回列表