我是如何把 API 响应时间从 200ms 压到了 10ms

少年呀,当你遇到这样的情况:API 慢得像蜗牛,P95 延迟超高,服务器在凌晨 3 点因为流量突发而崩溃,你是选择花三个月用 Rust 重写所有东西,还是选择看着用户流失呢。

或者,你可以像我一样,用一种作弊的方式,把 Bun 的极致速度嫁接到 Node.js 的庞大生态上。

别笑,我是认真的。我就能在不重写 5 年陈旧业务逻辑的前提下,把一个臃肿的后端接口压进 10ms 以内的。

边缘侧使用 Bun,通过 IPC 唤醒 Node 进程池

众所周知,Node 处理 HTTP 请求的开销太大了,但我的业务逻辑里全是依赖 crypto 和老旧 SDK 的代码,根本没法移植到 Bun。

所以我的解决办法是前店后厂。

我用 Bun 搭建了一个极薄的 HTTP 层,专门负责路由、参数校验和挡掉无效请求。只有真正需要那个老旧业务逻辑时,我才通过 IPC(进程间通信)把任务扔给后台常驻的 Node 进程。

千万别在请求来的时候才 spawn 一个 Node 进程,那样比单用 Node 还慢。你要做的是预先启动一组 Node Worker,要先预热才行。。

Bun 端(前台):

typescript 复制代码
// 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 });
  },
});

Node 端(后台):

javascript 复制代码
// 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));
});

这样搞,路由和 I/O 也是亚毫秒级的,而 Node 只需要处理纯计算,效率直接翻倍。

别让 CPU 搬砖,学会利用 Bun 的零拷贝特性

我发现服务器 CPU 居高不下,居然是因为我们在读取本地的配置文件和静态 JSON,然后序列化发给用户。

在 Node 里,通常会 fs.readFile 然后 res.send。这中间发生了好几次数据拷贝:从磁盘到内核,到用户空间 buffer,再到 socket。

在 Bun 里,我改用 Bun.file()。这不仅是写法上的区别,这是直接告诉操作系统:"把这个文件扔到网卡上去,别经过我的手。"

typescript 复制代码
// 别再 readFile 了,直接流式传输
Bun.serve({
  fetch(req) {
    if (req.url.endsWith("/config")) {
      return new Response(Bun.file("./big-config.json"));
    }
    return new Response("404");
  }
});

这一行代码改动,让我的静态资源吞吐量提升了 3 倍。

排好队,微批处理(Micro-batching)

高并发最可怕的是什么?是 1000 个请求同时涌进来,每个都要单独去调一次数据库或者调一次 Node 进程。就像刚下课,一堆学生全涌到食堂打饭。

而我加了一个极小的缓冲窗口。

如果在 3 毫秒内来了 50 个请求,我把它们打包成一个数组,一次性发给 Node 或者数据库。

typescript 复制代码
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 的延迟用户无感,但吞吐量巨大提升
  }
}

这 3 毫秒的等待,换来的是 CPU 负载降低 60%。

别在循环里 new 对象,求你了

我审查代码时发现,很多人喜欢在 fetch 或者 handleRequest 里写 const db = new DatabaseClient() 或者 const regex = new RegExp(...)

每次请求都重新分配内存、建立连接、编译正则,GC(垃圾回收)不炸才怪。

把所有能复用的东西------数据库连接池、TextEncoder、正则表达式、加密 Key,全部提到全局作用域。在 Bun 和 Node 混合架构里,这一点非常重要,因为我们追求的是极致的低延迟。

双层缓存:内存不够,磁盘来凑

以前我只用 Redis,但网络请求还是有开销。后来我发现,Bun 读取文件的速度超级快。

于是我搞了个双层缓存:

  1. L1 内存缓存:用 LRU 存最热的 1000 个 Key,微秒级响应。
  2. L2 文件缓存 :把稍微冷一点的数据直接写成 JSON 文件放在 /tmp/cache/ 下。

检查文件是否存在,比发起一个 TCP 请求去连 Redis 要快得多。

丢掉那些臃肿的 npm 包

以前在 Node 里,为了生成个 UUID 或者解析个参数,我们习惯性 npm install uuid 或者 qs

在 Bun 里(其实现代 Node 也是),crypto.randomUUID()URLSearchParams 都是内置的,而且是 C++ 层面优化的。

我把代码里所有非必要的 npm 依赖全部剔除,改用原生 API。这不仅让冷启动快了,更重要的是减少了 node_modules 的 I/O 噩梦。

解决精神分裂的开发环境

这一套架构就是Bun 做网关,Node 做计算。但在本地开发时,我差点崩溃。

我的电脑上本来跑着 Node 22,为了维护老项目,又要装 Node 14,还要装 Bun,甚至偶尔还要用 Deno 跑个脚本。

nvm 切换来切换去让我心力交瘁,端口冲突、路径报错、环境变量乱成一锅粥。我经常是修好了 Bun 环境,Node 的老项目又跑不起来了。

直到我发现了 ServBay,开发者的救命稻草,它不是那种简陋的版本切换器,它是一个完整的、隔离的运行环境平台。

  • 多版本并存:我可以同时开启 Node 14、Node 22 和 Bun 1.1 的环境,它们之间完全隔离,互不打架。
  • 一键全家桶:我需要的 Redis(做缓存)、PostgreSQL(存数据)、Caddy(做反代),它全都能一键安装并运行。
  • 零配置:我不由得感叹,以前为了配 Docker 和 Homebrew 浪费了不少时间啊。

有了 ServBay,我在本地完美复刻了线上的混合架构:Bun 监听 3000 端口,Node 监听内部管道,Redis 跑在后台。我再也不用担心这是环境问题还是代码问题了。

总结

只要能把响应时间压进 10ms,我不在乎混用多少种运行时。

Bun 给了我速度,Node 给了我稳定性,ServBay 给了我一个不发疯的开发环境

别再纠结用 Bun 还是用 Node.js了,都成年人了,为什么不能两个都要。把它们结合起来,现在就去把你的 API 延迟砍掉 90%。

相关推荐
mCell4 小时前
如何零成本搭建个人站点
前端·程序员·github
mCell5 小时前
为什么 Memo Code 先做 CLI:以及终端输入框到底有多难搞
前端·设计模式·agent
恋猫de小郭5 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
少云清5 小时前
【安全测试】2_客户端脚本安全测试 _XSS和CSRF
前端·xss·csrf
银烛木6 小时前
黑马程序员前端h5+css3
前端·css·css3
m0_607076606 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
听海边涛声6 小时前
CSS3 图片模糊处理
前端·css·css3
IT、木易6 小时前
css3 backdrop-filter 在移动端 Safari 上导致渲染性能急剧下降的优化方案有哪些?
前端·css3·safari
0思必得06 小时前
[Web自动化] Selenium无头模式
前端·爬虫·selenium·自动化·web自动化
青云计划6 小时前
知光项目知文发布模块
java·后端·spring·mybatis