我是如何把 API 响应时间从 200ms 压到了 10ms
少年呀,当你遇到这样的情况:API 慢得像蜗牛,P95 延迟超高,服务器在凌晨 3 点因为流量突发而崩溃,你是选择花三个月用 Rust 重写所有东西,还是选择看着用户流失呢。 或者,你可以像我一样,用一种作弊的方式,把 Bun 的极致速度嫁接到 Node.js 的庞大生态上。 别笑,我是认真的。我就能在不重写 5 年陈旧业务逻辑的前提下,把一个臃肿的后端接口压进 10ms 以内的。 众所周知,Node 处理 HTTP 请求的开销太大了,但我的业务逻辑里全是依赖 所以我的解决办法是前店后厂。 我用 Bun 搭建了一个极薄的 HTTP 层,专门负责路由、参数校验和挡掉无效请求。只有真正需要那个老旧业务逻辑时,我才通过 IPC(进程间通信)把任务扔给后台常驻的 Node 进程。 千万别在请求来的时候才 Bun 端(前台): Node 端(后台): 这样搞,路由和 I/O 也是亚毫秒级的,而 Node 只需要处理纯计算,效率直接翻倍。 我发现服务器 CPU 居高不下,居然是因为我们在读取本地的配置文件和静态 JSON,然后序列化发给用户。 在 Node 里,通常会 在 Bun 里,我改用 这一行代码改动,让我的静态资源吞吐量提升了 3 倍。 高并发最可怕的是什么?是 1000 个请求同时涌进来,每个都要单独去调一次数据库或者调一次 Node 进程。就像刚下课,一堆学生全涌到食堂打饭。 而我加了一个极小的缓冲窗口。 如果在 3 毫秒内来了 50 个请求,我把它们打包成一个数组,一次性发给 Node 或者数据库。 这 3 毫秒的等待,换来的是 CPU 负载降低 60%。 我审查代码时发现,很多人喜欢在 每次请求都重新分配内存、建立连接、编译正则,GC(垃圾回收)不炸才怪。 把所有能复用的东西——数据库连接池、 以前我只用 Redis,但网络请求还是有开销。后来我发现,Bun 读取文件的速度超级快。 于是我搞了个双层缓存: 检查文件是否存在,比发起一个 TCP 请求去连 Redis 要快得多。 以前在 Node 里,为了生成个 UUID 或者解析个参数,我们习惯性 在 Bun 里(其实现代 Node 也是), 我把代码里所有非必要的 npm 依赖全部剔除,改用原生 API。这不仅让冷启动快了,更重要的是减少了 这一套架构就是Bun 做网关,Node 做计算。但在本地开发时,我差点崩溃。 我的电脑上本来跑着 Node 22,为了维护老项目,又要装 Node 14,还要装 Bun,甚至偶尔还要用 Deno 跑个脚本。 直到我发现了 ServBay,开发者的救命稻草,它不是那种简陋的版本切换器,它是一个完整的、隔离的运行环境平台。 有了 ServBay,我在本地完美复刻了线上的混合架构:Bun 监听 3000 端口,Node 监听内部管道,Redis 跑在后台。我再也不用担心这是环境问题还是代码问题了。 只要能把响应时间压进 10ms,我不在乎混用多少种运行时。 Bun 给了我速度,Node 给了我稳定性,ServBay 给了我一个不发疯的开发环境。 别再纠结用 Bun 还是用 Node.js了,都成年人了,为什么不能两个都要。把它们结合起来,现在就去把你的 API 延迟砍掉 90%。
边缘侧使用 Bun,通过 IPC 唤醒 Node 进程池
crypto 和老旧 SDK 的代码,根本没法移植到 Bun。spawn 一个 Node 进程,那样比单用 Node 还慢。你要做的是预先启动一组 Node Worker,要先预热才行。。// bun-gateway.ts
const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();
// 启动一个常驻的 Node 进程,而不是每次请求都启动
const nodeWorker = Bun.spawn(["node", "heavy-lifter.js"], {
stdin: "pipe",
stdout: "pipe",
});
// 这是一个简单的读写封装,把复杂的脏活扔过去
async function askNode(payload: any) {
const msg = JSON.stringify(payload) + "\n";
nodeWorker.stdin.write(textEncoder.encode(msg));
// 这里简化了读取逻辑,生产环境记得处理粘包
const reader = nodeWorker.stdout.getReader();
const { value } = await reader.read();
return JSON.parse(textDecoder.decode(value));
}
Bun.serve({
port: 3000,
async fetch(req) {
if (req.url.endsWith("/fast")) return new Response("Bun is fast!");
// 只有这种重活才找 Node
if (req.url.endsWith("/heavy")) {
const data = await req.json();
const result = await askNode(data);
return Response.json(result);
}
return new Response("404", { status: 404 });
},
});// heavy-lifter.js
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
rl.on('line', (line) => {
const data = JSON.parse(line);
// 假装我们在做一个很重的加密运算
// Node 生态里的老代码都在这儿跑,不用改
const result = { processed: true, echo: data };
console.log(JSON.stringify(result));
});别让 CPU 搬砖,学会利用 Bun 的零拷贝特性
fs.readFile 然后 res.send。这中间发生了好几次数据拷贝:从磁盘到内核,到用户空间 buffer,再到 socket。Bun.file()。这不仅是写法上的区别,这是直接告诉操作系统:“把这个文件扔到网卡上去,别经过我的手。”// 别再 readFile 了,直接流式传输
Bun.serve({
fetch(req) {
if (req.url.endsWith("/config")) {
return new Response(Bun.file("./big-config.json"));
}
return new Response("404");
}
});排好队,微批处理(Micro-batching)
let buffer: any[] = [];
let timer: Timer | null = null;
function processBatch() {
const currentBatch = buffer;
buffer = [];
timer = null;
// 一次性把 50 个任务发给 Node,而不是发 50 次
askNode({ type: 'batch', items: currentBatch });
}
function enqueue(item: any) {
buffer.push(item);
// 只有在第一次推进来时启动计时器
if (!timer) {
timer = setTimeout(processBatch, 3); // 3ms 的延迟用户无感,但吞吐量巨大提升
}
}别在循环里
new 对象,求你了fetch 或者 handleRequest 里写 const db = new DatabaseClient() 或者 const regex = new RegExp(...)。TextEncoder、正则表达式、加密 Key,全部提到全局作用域。在 Bun 和 Node 混合架构里,这一点非常重要,因为我们追求的是极致的低延迟。双层缓存:内存不够,磁盘来凑
/tmp/cache/ 下。丢掉那些臃肿的 npm 包
npm install uuid 或者 qs。crypto.randomUUID()、URLSearchParams 都是内置的,而且是 C++ 层面优化的。node_modules 的 I/O 噩梦。解决精神分裂的开发环境
nvm 切换来切换去让我心力交瘁,端口冲突、路径报错、环境变量乱成一锅粥。我经常是修好了 Bun 环境,Node 的老项目又跑不起来了。

总结