参考

性能

采样配置
evlog 每个请求增加约 3µs 开销。比 pino、consola 和 winston 更快(在大多数场景下),同时发出更丰富、更有用的事件。

evlog 每个请求增加 约 3µs 的开销,也就是 0.003ms,远低于任何 HTTP 框架或数据库调用的量级。性能在每次拉取时都会通过 CodSpeed 进行跟踪。

evlog 与其他方案对比

所有基准测试均使用 JSON 输出到无操作目标。pino 写入 /dev/null(同步),winston 写入无操作流,consola 使用无操作报告器,evlog 使用静默模式。

结果

wide event lifecycle · ops/sec·measuring
throughputcreate + 3× set + emit
evlogn/a ops/s
pinon/a ops/s
consolan/a ops/s
winstonn/a ops/s
consola has no equivalent (no wide-event API) — see the table below for fair scenarios.
why · 4 lines → 1 event0 / 4 lines
child.info({ user: { id, plan } }, 'user context')
child.info({ cart: { items, total } }, 'cart context')
child.info({ payment: { method } }, 'payment context')
child.info({ status: 200 }, 'request complete')
evlog wide event
{
  user:    { id, plan },
  cart:    { items, total },
  payment: { method },
  status:  200
}
75%less data on the wire1 row to query
vs pino7.7× faster
vs winston14.1× faster
CI trackingCodSpeed · per PR
Scenarioevlogpinoconsolawinston
Simple string log1.83M ops/s1.09M2.79M1.20M
Structured (5 fields)1.64M ops/s716.1K1.71M431.6K
Deep nested log1.55M ops/s464.9K1.01M164.0K
Child / scoped logger1.70M ops/s845.0K280.4K430.0K
Wide event lifecycle1.58M ops/s205.8K111.9K
Burst (100 logs)17.8K ops/s10.3K39.4K7.5K
Logger creation16.85M ops/s7.50M310.3K5.38M

evlog 在 7 次一对一比较中赢了 4 次,而且最重要的胜利都很 निर्ण然:在广泛事件模式下比 pino 快 7.7 倍,日志创建 快 2.3 倍,深层嵌套日志 快 3.3 倍。consola 在简单字符串和突发场景中略胜一筹(它使用不做序列化的无操作报告器),但 evlog 为每个请求生成一条相关联事件,而传统日志记录器会发出 N 条独立日志行。

为什么这很重要:在广泛事件模式下(每个请求一个事件,是真实世界 API 的形态),evlog 比 pino 快 7.7 倍,比 winston 快 14.1 倍,同时向你的日志接收器发送的数据量减少 75%,并且给你的是一个可查询事件,而不是 4 条彼此断开的日志行。7.7 倍并不是蛮力取胜——pino 并不会尝试累积上下文,因此这里的比较反映的是架构差异,而不是公平性问题。请参阅 evlog 可能不会获胜的情况 了解诚实的差距。

什么是“广泛事件生命周期”?

该基准测试模拟了一个真实的 API 请求:

const log = createLogger({ method: 'POST', path: '/api/checkout', requestId: 'req_abc' })
log.set({ user: { id: 'usr_123', plan: 'pro' } })
log.set({ cart: { items: 3, total: 9999 } })
log.set({ payment: { method: 'card', last4: '4242' } })
log.emit({ status: 200 })

相同的 CPU 开销,但 evlog 将所有内容集中在一个位置。

为什么 evlog 更快?

上述数字并非魔法,源于深思熟虑的架构选择:

原地修改,而非复制。 log.set() 通过递归的 mergeInto 函数直接写入上下文对象。其他日志记录器在每次调用时克隆对象(使用对象展开或 Object.assign)。evlog 在上下文累积过程中从不分配中间对象。

直到输出阶段才序列化。 上下文在整个请求生命周期中保持为普通 JavaScript 对象。JSON.stringify 仅在输出时运行一次。传统日志记录器在每次 .info() 调用时都进行序列化,4 条日志行意味着 4 次序列化。

惰性分配。 时间戳、采样上下文和覆盖对象仅在真正需要时创建。如果尾采样被禁用(常见情况),其上下文对象永远不会被分配。用于 ISO 时间戳的 Date 实例在多次调用间复用。

一条事件,而非 N 行。 对于典型请求,pino 会发出 4 条以上的 JSON 行,每条都需要序列化、传输和索引。evlog 只发出一条。这样为日志接收器减少 75% 的工作量,减少传输中的字节数,并且查询时只需处理一行而非四行。

正则表达式缓存。 全局模式(用于采样和路由匹配)只编译一次并缓存。重复评估命中缓存而非重新编译。

当 evlog 可能不会获胜时

上述基准测试衡量的是主线程上的 CPU + 序列化成本,没有真实 I/O。这是 pino、winston 和 logtape 在各自基准测试中采用的标准设置——但它遗漏了少数一些其他日志记录器可能略胜一筹的场景。对此应当诚实:

使用 worker-thread 的 pino 快速发送路径。 在生产环境中,pino 通常会配置 worker-thread transportpino-prettypino-loki、厂商特定 transport)。序列化和 I/O 会完全转移到主线程之外。对于每秒发出数十万条 log.info('foo') 且不累积上下文的工作负载,pino-via-worker 在主线程上可达到约 2-3M ops/s,因为它只是排队。我们无法在单线程 vitest 进程中公平地对该模式进行基准测试,因此它不在表中——但这确实是 pino 更快的一个真实场景。

仅 CLI / 仅美化输出且不序列化。 我们基准测试中的 consola 无操作报告器模式(level: 4, reporters: [{ log: () => {} }])完全跳过了 JSON 序列化。如果你使用 consola 做仅终端输出的 CLI,这很现实,但这也是 consola 在“简单字符串”和“突发”场景中获胜的原因——它做的工作并不相同。evlog 和 pino 都会序列化为 JSON;而这些基准中的 consola 不会。如果你的使用场景是“美观的终端输出,不向任何地方发送日志”,consola 确实更轻量。

单次 log.info 调用,不累积上下文。pino.info('hello')evlog.info('hello') 的比较中,evlog 和 pino 大致打平(在我们的运行中分别是 1.83M vs 1.09M ops/s,但如果 pino 运行在异步模式下,差距会进一步缩小)。evlog 的约 7.7 倍优势具体体现在你本来会为一次逻辑操作发出 N 条独立日志行的场景。如果你确实是每次调用只记一行,而且不累积上下文,那么速度差距要小得多——选择 evlog 应该是因为 API 体验(log.set + 结构化错误),而不是原始吞吐量。

壁钟时间波动是真实存在的。 同一台机器上,不同运行之间,Vitest bench 数值会有 ±5-10% 的波动(热降频、GC、其他进程)。上述数字来自 MacBook 上的一次运行;CI 通过 CodSpeed 的 CPU 指令计数进行回归跟踪(确定性、±0.5% 噪声地板),但本页中的绝对 hz 值是壁钟快照,而不是有保证的下限。

结论是:对于广泛事件模式,这些胜利是真实的,但如果你的栈是“纯粹使用 worker transport 的 pino 快速发送”,那就是我们不声称能胜过的唯一场景。

真实世界开销

对于典型 API 请求:

组件开销
Logger creation52ns
3x set() calls105ns
emit()588ns
Sampling22ns
Enricher pipeline2.14µs
Total~2.9µs

作为参考,数据库查询需 1-50ms,HTTP 调用需 10-500ms。evlog 的开销几乎不可察觉

打包体积

每个入口点都支持 tree-shaking。你只为你导入的部分付费。

入口Gzip 体积
core (evlog)510 B
toolkit (evlog/toolkit)720 B
utils1.58 kB
error1.46 kB
enrichers1.99 kB
pipeline1.35 kB
http1.22 kB
browser289 B
workers1.30 kB
client128 B

典型的 Node.js 包(initLogger + createLogger)在 tree-shaking 之后端到端大小约为 ~6.3 kB gzip;再加入 createRequestLoggercreateErrorparseErroruseLogger 后,包体积会达到 ~7.2 kB gzip。适配器和框架集成位于其上:Hono 为 617 B,Express 为 734 B,Axiom 为 1.48 kB。每个 PR 都会跟踪包体积,并与 main 基线进行比较。

详细基准测试

日志创建

操作ops/sec平均值
createLogger() (no context)19.20M52ns
createLogger() (shallow context)18.74M53ns
createLogger() (nested context)17.70M56ns
createRequestLogger() (method + path)16.91M59ns
createRequestLogger() (method + path + requestId)12.67M79ns

上下文累积(log.set()

操作ops/sec平均值
Shallow merge (3 fields)9.56M105ns
Shallow merge (10 fields)4.79M209ns
Deep nested merge8.04M124ns
4 sequential calls7.05M142ns

事件发射(log.emit()

操作ops/sec平均值
Emit minimal event1.93M519ns
Emit with context1.70M588ns
Full lifecycle (create + 3 sets + emit)1.59M628ns
Emit with error65.9K15.17µs
发射带错误 更慢,因为 Error.captureStackTrace() 是昂贵的 V8 操作(约 15µs)。仅当抛出错误时触发。

负载扩展

负载ops/sec平均值
Small (2 fields)1.72M581ns
Medium (50 fields)569.8K1.76µs
Large (200 nested fields)131.2K7.62µs

采样

操作ops/sec平均值
Tail sampling (shouldKeep)44.97M22ns
Full emit with head + tail7.01M143ns

增强器

增强器ops/sec平均值
User Agent (Chrome)2.61M384ns
Geo (Vercel)3.88M258ns
Request Size12.37M81ns
Trace Context4.35M230ns
All combined (all headers)466.7K2.14µs

错误处理

操作ops/sec平均值
createError()232.2K4.31µs
parseError()45.48M22ns
Round-trip (create + parse)231.4K4.32µs

Middleware pipeline

Operationops/secMean
resolveMiddlewarePluginRunner (no plugins)37.70M27ns
resolveMiddlewarePluginRunner (2 plugins, cached)32.26M31ns
createMiddlewareLogger (no plugins, safe headers)4.41M227ns
createMiddlewareLogger (2 plugins, cached merge)4.13M242ns
Full request lifecycle (no plugins, no drain)993.7K1.01µs
Full request lifecycle (2 plugins, sync drain)621.2K1.61µs

方法论与可信度

你能相信这些数字吗?

本页面上的每个基准测试都是开源的可复现的。基准测试文件位于 packages/evlog/bench/。你可以阅读确切代码、在本机运行并验证结果。

所有库在相同条件下测试:

  • 相同输出模式:JSON 输出到无操作目标(不测量磁盘或网络 I/O)
  • 相同预热:每个基准测试在 JIT 稳定后运行 500ms
  • 相同工具Vitest bench,由 tinybench 提供支持
  • 相同机器:比较库时,所有基准测试在同一进程的同一硬件上运行

CI 回归跟踪

性能回归在每次拉取请求时通过两个系统跟踪:

  • CodSpeed 使用 CPU 指令计数运行所有基准测试(而非壁钟时间)。这消除了共享 CI 运行器中的噪声,并产生确定性、可复现的结果。回归直接在 PR 上标记。
  • 打包大小比较 测量所有入口点与 main 基线的对比,并在 PR 评论中发布大小差异报告。

自行运行

Terminal
cd packages/evlog

pnpm run bench                          # 所有基准测试
pnpm exec vitest bench bench/comparison/ # 仅与替代方案对比
pnpm exec tsx bench/scripts/size.ts     # 包体积