这一招让 Node 后端服务启动速度提升 75%!

一个Node 后端项目的启动方式可以分类为三种:

  1. 由源代码直接启动,如tsx src/server.ts
  2. 由tsc简单转译,如tsc 编译后 node dist/server.js
  3. 使用一些bundler进行打包,将其打包为单个文件,如esbuild --bundlenode bundle.mjs

很多人其实并不知道这几种方法之间的区别,今天我想通过具体的测试来区分每种方法的不同。

测试目的

把测试拆成两个维度:

  • 启动阶段性能
  • 服务运行阶段性能

因为,我们可以测试三类数据:

  1. 冷启动会差多少?
  2. 稳态吞吐/延迟会差多少?
  3. 资源占用是否存在显著差异?

测试方法

环境

  • macOS arm64
  • Apple M5 / 10 cores
  • 24GB RAM
  • Node v22.22.0
  • Express 5.1(TypeScript)

三种模式使用同一份 src/server.ts,业务逻辑完全一致,包含基础路由和"mock业务形态"路由:

  • 基础路由(baseline)(参考了一个比较Express4/5版本速度差异的测试方法)

    • GET /ping:返回 "pong",用于测最小路径开销
    • GET /middlewares:挂 50 层 no-op middleware 后返回 { ok: true }
    • GET /json:返回预生成的约 50KB JSON(固定内容,避免每次动态生成噪声)
    • GET /payload:返回预生成的 100KB 文本
  • 业务形态路由(realistic)

    • GET /route1/info
    • GET /route2/stats
    • GET /route3/catalog
    • GET /route4/summary(聚合 route1~3 的服务输出)
    • GET /orm/users(走 Drizzle ORM 查询路径)

ORM 使用 drizzle-orm/sqlite-proxy + 内存数据模拟,不依赖外部数据库,尽量隔离网络与 DB 抖动对对比的干扰。

压测与采样口径

  • 压测命令:ab -k -n 200000 -c 100
  • 每个场景重复 5 次,取均值
  • 冷启动定义:从进程 spawn/ping 首个 200
  • 资源采样:压测期间每秒采样 RSSCPU%

总体结果

1)冷启动

mode cold(ms)
esbuild 104
tsc 308
tsx 399

冷启动的差异非常明显,esbuild 在冷启动上的表现优于 tsctsx,差距达到 75%。

2)平均吞吐(9 场景)

mode avg req/s
tsc 22114
esbuild 21959
tsx 21928

吞吐量差异小于 1%,可见三者在稳态性能上几乎一致。

3)P95 延迟

三种方式的 P95 延迟几乎完全相同,均为 5-6ms。

4)RSS 内存

三者的内存使用几乎一致,均约为 62MB。

测试数据报告:链接


关键问题 1:为什么冷启动差这么多?

冷启动时间的差异可以拆解为以下几个部分:

  1. 模块图加载与文件 IO

    a. 解析 import

    • 静态分析 :Node.js 会分析你的 JavaScript 文件中的 import 语句,确定需要加载的模块。这是一个静态分析过程,Node.js 会在执行之前构建模块的依赖关系图(import 图)。这有助于了解哪些模块需要加载,并准备好这些模块的依赖。

    b. 读取文件

    • 加载文件 :当 Node.js 发现一个 import 语句,它会根据静态分析结果读取相应的文件内容。如果该模块是一个 JavaScript 文件(.js.mjs),Node.js 会读取文件的内容并将其解析为 JavaScript 代码。
    • 查找模块:Node.js 会查找模块文件的位置,如果模块没有缓存,它会从磁盘读取相应文件。

    c. 构建模块缓存

    • 模块缓存:Node.js 会缓存已加载的模块,这样在多次加载同一个模块时,Node.js 不需要重新执行该模块的代码。这样可以提高性能,避免重复加载和执行相同的模块。
    • 导出模块 :在加载并执行完模块后,Node.js 会将模块的导出结果(module.exportsexport)存入缓存中,以便后续调用。

在不同的启动方式下,加载的模块数量和方式有所不同:

  • tsc:编译多个 JS 文件。
  • tsx:编译多个 TS 文件。
  • esbuild:生成单一的打包文件。

esbuild 通过打包将多个模块合并为一个文件,减少了模块解析和文件 I/O 的开销,因此冷启动时间显著较短。

  1. 运行时转译成本(仅适用于 tsx)

tsx 使用 esbuild 编译 TypeScript 和 ESM,还会生成 source map 并内联到代码中。每次启动时,tsx 需要额外进行源代码映射,导致启动速度较慢。而 tsc 编译的是纯 JavaScript,Node.js 不需要做任何 TypeScript 转换,启动速度较快。

冷启动的结论

边缘计算、Serverless、短生命周期容器、CLI 工具,这种场景下,冷启动的速度至关重要,那使用esbuild等打包工具提前bundler而带来的冷启动优势是实打实的。

如果是常驻 API 服务,冷启动只发生一次,意义有限。

但是天下毕竟没有免费的午餐。使用第三方bundle工具提前bundle是不是也有一些坏处呢?是的。

第一点,不支持一些 TypeScript 特性。如esbuild不支持保留如eval()语法,还有就是不支持某些tsconfig.json属性,如emitDecoratorMetadata

第二点,调试难度加大。esbuild 生成的代码通常会做大量的代码压缩、优化和打包,这使得调试变得比较困难。因为调试时的代码结构与原始源代码有很大的差异。如线上报错,开发人员可能需要额外的源映射(source maps)和调试工具来简化调试过程。

第三点,就是启动时,会有更大的cpu运行开销,请看下一节。

关键问题 2:为什么 CPU 峰值差异大?

CPU 峰值均值:

mode peak CPU%
tsx 5.84
tsc 9.13
esbuild 11.89

这看起来 esbuild 更"耗 CPU"。但吞吐几乎一样。这说明什么?

一个可能解释是:在使用 esbuild 打包后的代码中,代码结构变得更加紧凑,启动时可能会大量导入很多原来零散的js模块等,某些常见的函数或代码路径可能会执行得更加频繁。

由于 JIT 编译机制,V8 可能会更快地识别出这些频繁执行的代码,并对其进行优化。这个优化过程又叫热点编译。

V8 JIT(即时编译)

  • V8 是 Chrome 和 Node.js 中使用的 JavaScript 引擎,它使用 JIT(即时编译,Just-In-Time Compilation) 技术将 JavaScript 代码在运行时编译成机器代码,来提高执行效率。
  • JIT 编译的目的是将频繁执行的代码(即"热点代码")优化成更高效的机器代码,从而提升性能。

热点编译(Hotspot Compilation)

  • 当你执行一段 JavaScript 代码时,V8 会在开始时使用 解释执行(即不进行优化的方式)来快速运行代码。
  • 如果某段代码被执行得非常频繁(即"热点代码"),V8 会将它标记为热点代码,并对其进行优化。
  • 这时,V8 会在后台将热点代码编译为更高效的机器码,称为 热点编译。这通常会提高执行速度,但也可能带来一些额外的 CPU 开销。

在 V8 进行热点编译时,它需要使用 CPU 来分析和优化这些热点代码。这通常会导致短时间内 CPU 使用率升高,表现为 CPU 使用率的"抬高"。

另外很重要的一点,可以看到上面的统计图表中,重要的一点是,吞吐量几乎一致 ,这意味着无论是 tsxtsc 还是 esbuild,在处理请求时的效率差异都很小。如果 esbuild 确实比其他模式更高效,它的吞吐量应该显著超过其他模式。然而,实际数据表明,差距微乎其微,这表明 CPU 峰值差异 主要来源于 短期的计算开销,而非整体的运行效率差异。

最终性能,尤其是吞吐量,在底层上受 V8 引擎优化和 I/O 处理 等因素的影响更大,在运行层面上应该受到业务逻辑、IO、JSON 序列化、数据库等因素决定。

小结

现在可以回答问题Node 后端服务启动方式的问题了:

在开发环境,生产环境(常驻 API 服务),还是推荐 tsx。性能差别不大,带来了更好的体验。

在如云函数等,冷启动敏感场景,推荐用如 esbuild来提前bundle,本文中的案例,esbuild的冷启动时长比普通tsx快了75%!

相关推荐
jonjia18 小时前
模块、脚本与声明文件
typescript
jonjia18 小时前
配置 TypeScript
typescript
Mr_li18 小时前
NestJS 集成 TypeORM 的最优解
node.js·nestjs
jonjia18 小时前
TypeScript 工具函数开发
typescript
jonjia18 小时前
注解与断言
typescript
jonjia18 小时前
IDE 超能力
typescript
jonjia18 小时前
对象类型
typescript
jonjia18 小时前
快速搭建 TypeScript 开发环境
typescript
jonjia18 小时前
TypeScript 的奇怪之处
typescript