Fastify 为什么比 Express 快五倍:架构设计与性能优化原理

本文分析 Fastify 比 Express 快数倍背后的架构设计与性能优化原理,拆解 JSON Schema 序列化、前缀字典树路由、插件生命周期等核心差异,并讨论框架选型中的性能误区与落地建议。

从一个 Benchmark 说起

很多团队在选择 Node.js Web 框架时,都见过 Fastify 的 Benchmark 优势。在某些压测场景里,Fastify 的吞吐量能比 Express 高出数倍,“快五倍”的说法也是从这类测试里来的。但这里要先泼一盆冷水:线上接口的响应时间通常由数据库查询、第三方服务和网络延迟决定,框架本身的 CPU 开销只占一小部分。假设一个接口总耗时 300ms,数据库占 250ms,框架只占 10ms,那框架再快五倍,用户能感知到的变化也微乎其微。

AI technology illustration

所以,讨论“Fastify 为什么更快”之前,有必要先弄清快的是哪一部分。Fastify 的快主要体现在请求处理路径上的 CPU 时间,而不是 I/O 时间。理解了这一点,再看架构设计就不会被 Benchmark 带偏。

Express 与 Fastify 的架构分水岭

Express 出现得早,它的设计目标很简单:把 Node.js 的 HTTP 处理包装成一种直观的中间件模型。中间件本质上是按顺序执行的函数数组,路由匹配也在这个数组里依次进行。这种设计很好懂,但有一个隐藏成本:当路由数量增加到几百个之后,一个请求可能要经过很多次正则匹配才能真正命中 handler。路由越复杂,线性查找的耗时就越明显。

Fastify 的出发点和 Express 不同,它看起来更“重”,因为它从一开始就把性能作为架构约束。最典型的路由匹配,Fastify 使用 find-my-way 实现的前缀字典树。相同前缀的路由会共享路径分支,查找代价基本取决于 URL 的层数,而不是路由总数。一个大型服务里,这能稳定减少几十次不必要的正则判断。

但路由匹配只是表象,真正影响深远的是框架对运行时信息的掌握程度。Fastify 要求你在启动阶段声明很多结构,比如路由的 schema、插件的依赖范围。这样做等于把很多原本在运行时动态判断的事情,提前到了启动阶段完成。

Fastify 真正快在哪些设计上

JSON Schema 预编译序列化

最常见的性能差异来自 JSON 响应序列化。Express 的 res.json 最终调用 JSON.stringify,它必须在每次请求时检查字段类型、遍历对象属性,这个过程对 V8 来说并不便宜。Fastify 允许在路由上声明 response schema,启动时用 fast-json-stringify 生成一个只针对该结构的序列化函数。序列化函数知道每个字段的类型,可以直接拼接字符串,省掉大量动态判断。

// Express 路由:响应序列化交给 JSON.stringify
router.get('/user/:id', async (req, res) => {
  const user = await db.getUser(req.params.id);
  res.json(user);
});

// Fastify 路由:schema 被预编译为专用序列化器
fastify.get('/user/:id', {
  schema: {
    response: {
      200: {
        type: 'object',
        properties: {
          id: { type: 'integer' },
          name: { type: 'string' }
        }
      }
    }
  }
}, async (req, reply) => {
  const user = await db.getUser(req.params.id);
  return user;
});

这个例子的信息量不小。Fastify 的 schema 不仅是参数校验,还会参与到响应生成中。字段多了以后,预编译序列化函数的优势会更明显,因为 JSON.stringify 对嵌套对象的反射开销是按比例增长的。

前缀字典树路由

我再补充一点 find-my-way 的设计。Express 的路由表是扁平数组,匹配时按注册顺序遍历。Fastify 的路由表是前缀字典树,/user 和 /user/:id 会共享前段路径,查找时不需要逐个测试所有路由。路由越多,树形结构的优势越大。另外,find-my-way 对静态路由、参数路由做了优先级区分,尽量避免把一个静态 URL 当成参数去匹配。这些细节叠加起来,在高并发下能省出不少 CPU 时间。

生命周期让热路径更收敛

Fastify 将请求处理拆成了 onRequest、preHandler、handler、onSend 等几个阶段。表面上看这只是规范,实际上它让中间件不再是一条无法预测的链。每个阶段都可以明确知道当前上下文和数据,错误处理被集中到统一出口。这样请求要么很快失败,要么很快走完核心逻辑,不会在中间件链上反复包裹 Promise 和上下文。

还有一个容易被忽略的地方。Express 习惯里,往 req 对象上挂属性太常见了,req.user、req.session 到处都是。这种动态添加属性会让 V8 的对象隐藏类优化失效。Fastify 因为插件和装饰器机制,把共享数据放到内部管理的 context 中,减少请求对象形状被随意修改的可能。这个设计不是限制自由,而是在保护 V8 的优化能力。

Fastify 与 Express 的取舍对照

维度 Fastify Express
路由匹配 前缀字典树,复杂度与路由总数弱相关 顺序正则匹配,路由越多耗时越高
JSON 序列化 基于 schema 预编译序列化器 默认 JSON.stringify,运行期反射
插件模型 插件作用域封装,上下文可控 中间件全局链,灵活但容易乱
生命周期 阶段清晰,错误出口统一 线性回调链,处理顺序敏感
开发约束 需要维护 schema,理解插件上下文 上手快,但大项目复杂度靠自觉
适用场景 网关、BFF、高吞吐服务 原型、复杂中间件编排、遗留项目

这张表不是为了证明 Fastify 全面优于 Express。对很多小型应用来说,Express 的灵活性和生态成熟度仍然是实打实的优势。Fastify 的收益需要一定规模或明确性能压力才会变得值得。

快不是免费的:Fastify 的代价与误区

性能优势背后一定有成本,Fastify 的成本比很多人想象中高。

  • 你必须维护 response schema。如果接口字段经常变动,额外工作量不会小。
  • schema 声明之外的字段不会被序列化。它不像 Express 一样“返回什么就输出什么”,这会导致一种新的调试困难。
  • 从 Express 迁移到 Fastify,不只是换个路由语法,还要理解插件作用域和生命周期。团队需要付出学习成本。

三个常见的性能误区

  • 误区一:认为所有服务换到 Fastify 都能快五倍。事实上,如果你的瓶颈在数据库或下游服务,框架优化的收益会被 I/O 等待吞掉。
  • 误区二:为了性能全面重写现有 Express 项目。重写期间的风险和回归成本往往比性能收益更大,尤其是那些稳定性优先的内部系统。
  • 误区三:只关注平均 QPS,忽略 p99 和 GC。Fastify 的优化也会在错误使用 schema 时产生不预期的字符串截断,压测时要把异常返回也纳入验证。

从哪个服务开始切换最合适

如果团队决定尝试 Fastify,不要从最复杂的核心业务开始,也不要把所有路由一次性迁过去。比较稳妥的做法是选下面这类服务试点:

  1. 网关或 BFF 层。这类服务通常路由多、业务轻,Fastify 的路由索引和序列化优势最容易体现。
  2. 响应结构稳定的内部服务。比如标准化的用户中心、配置中心,结构固定,schema 维护成本低。
  3. 短平快的性能敏感服务。比如实时排行榜、秒级聚合接口,能快速验证前后差异。

压测时除了看吞吐量,还要看 p99、内存增长和 GC 时间。很多团队换框架后平均 QPS 上去了,但 GC 抖动变多,这时候不能说整体变好了。

最后的判断

回到标题:Fastify 为什么比 Express 快?因为它把很多在 Express 运行期间动态完成的事情,提前到了启动阶段完成。它用更明确的契约换掉了隐式判断,用树形索引换掉了遍历式匹配,用编译期准备换掉了运行期解释。这是架构设计带来的性能优化,而不是某个神秘的“黑魔法”。

技术选型终究不是跑分游戏。Fastify 的昂贵之处在于它要求你遵守它的规则,而规则本身也是一种工程约束。如果你的服务已经足够简单,Express 的灵活和生态仍然会很舒服;如果你正在被路由规模、序列化开销和运行不确定性困扰,Fastify 的快就有真正的价值。

原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/590/

(0)
上一篇 2小时前
下一篇 1小时前

相关推荐