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

所以,讨论“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,不要从最复杂的核心业务开始,也不要把所有路由一次性迁过去。比较稳妥的做法是选下面这类服务试点:
- 网关或 BFF 层。这类服务通常路由多、业务轻,Fastify 的路由索引和序列化优势最容易体现。
- 响应结构稳定的内部服务。比如标准化的用户中心、配置中心,结构固定,schema 维护成本低。
- 短平快的性能敏感服务。比如实时排行榜、秒级聚合接口,能快速验证前后差异。
压测时除了看吞吐量,还要看 p99、内存增长和 GC 时间。很多团队换框架后平均 QPS 上去了,但 GC 抖动变多,这时候不能说整体变好了。
最后的判断
回到标题:Fastify 为什么比 Express 快?因为它把很多在 Express 运行期间动态完成的事情,提前到了启动阶段完成。它用更明确的契约换掉了隐式判断,用树形索引换掉了遍历式匹配,用编译期准备换掉了运行期解释。这是架构设计带来的性能优化,而不是某个神秘的“黑魔法”。
技术选型终究不是跑分游戏。Fastify 的昂贵之处在于它要求你遵守它的规则,而规则本身也是一种工程约束。如果你的服务已经足够简单,Express 的灵活和生态仍然会很舒服;如果你正在被路由规模、序列化开销和运行不确定性困扰,Fastify 的快就有真正的价值。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/590/