evlog 每个请求增加 约 3µs 的开销,也就是 0.003ms,远低于任何 HTTP 框架或数据库调用的量级。性能在每次拉取时都会通过 CodSpeed 进行跟踪。
evlog 与其他方案对比
所有基准测试均使用 JSON 输出到无操作目标。pino 写入 /dev/null(同步),winston 写入无操作流,consola 使用无操作报告器,evlog 使用静默模式。
结果
| 场景 | evlog | pino | consola | winston |
|---|---|---|---|---|
| 简单字符串日志 | 1.96M 次/秒 | 1.06M | 2.67M | 977.6K |
| 结构化(5 个字段) | 1.74M 次/秒 | 705.6K | 1.75M | 440.6K |
| 深度嵌套日志 | 1.75M 次/秒 | 507.8K | 1.04M | 202.5K |
| 子级 / 作用域日志 | 1.85M 次/秒 | 871.0K | 272.2K | 568.5K |
| 广泛事件生命周期 | 1.68M 次/秒 | 209.0K | — | 114.6K |
| 突发(100 条日志) | 19.1K 次/秒 | 10.0K | 40.8K | 7.6K |
| 日志创建 | 20.52M 次/秒 | 7.36M | 299.3K | 5.43M |
evlog 在 7 次直接对比中赢得 4 次,且关键优势明显:广泛事件生命周期中比 pino 快 8 倍,日志创建快 2.8 倍,深度嵌套日志快 3.5 倍。consola 在简单字符串和突发场景略胜一筹(它使用无操作报告器且无需序列化),但 evlog 为每个请求生成一条单一的相关事件,而传统日志生成 N 条独立行。
什么是“广泛事件生命周期”?
该基准测试模拟了一个真实的 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 })
const child = pinoLogger.child({ method: 'POST', path: '/api/checkout', requestId: 'req_abc' })
child.info({ user: { id: 'usr_123', plan: 'pro' } }, 'user context')
child.info({ cart: { items: 3, total: 9999 } }, 'cart context')
child.info({ payment: { method: 'card', last4: '4242' } }, 'payment context')
child.info({ status: 200 }, 'request complete')
相同的 CPU 开销,但 evlog 将所有内容集中在一个位置。
为什么 evlog 更快?
上述数字并非魔法,源于深思熟虑的架构选择:
原地修改,而非复制。 log.set() 通过递归的 mergeInto 函数直接写入上下文对象。其他日志记录器在每次调用时克隆对象(使用对象展开或 Object.assign)。evlog 在上下文累积过程中从不分配中间对象。
直到输出阶段才序列化。 上下文在整个请求生命周期中保持为普通 JavaScript 对象。JSON.stringify 仅在输出时运行一次。传统日志记录器在每次 .info() 调用时都进行序列化,4 条日志行意味着 4 次序列化。
惰性分配。 时间戳、采样上下文和覆盖对象仅在真正需要时创建。如果尾采样被禁用(常见情况),其上下文对象永远不会被分配。用于 ISO 时间戳的 Date 实例在多次调用间复用。
一条事件,而非 N 行。 对于典型请求,pino 会发出 4 条以上的 JSON 行,每条都需要序列化、传输和索引。evlog 只发出一条。这样为日志接收器减少 75% 的工作量,减少传输中的字节数,并且查询时只需处理一行而非四行。
正则表达式缓存。 全局模式(用于采样和路由匹配)只编译一次并缓存。重复评估命中缓存而非重新编译。
实际开销
对于典型 API 请求:
| 组件 | 开销 |
|---|---|
| 日志创建 | 49ns |
3 次 set() 调用 | 63ns |
emit() | 570ns |
| 采样 | 23ns |
| 增强器管道 | 2.05µs |
| 总计 | 约 2.8µs |
作为参考,数据库查询需 1-50ms,HTTP 调用需 10-500ms。evlog 的开销几乎不可察觉。
打包体积
每个入口点都支持 tree-shaking。你只为你导入的部分付费。
| 入口 | Gzip 体积 |
|---|---|
| logger | 3.78 kB |
| utils | 1.41 kB |
| error | 1.21 kB |
| enrichers | 1.92 kB |
| pipeline | 1.35 kB |
| http | 1.21 kB |
典型 Nuxt 配置加载 logger + utils,总体积约为 5.2 kB Gzip。打包体积在每次拉取时都会被跟踪并与 main 基线比较。
详细基准测试
日志创建
| 操作 | ops/sec | 平均值 |
|---|---|---|
createLogger()(无上下文) | 19.35M | 52ns |
createLogger()(浅层上下文) | 20.38M | 49ns |
createLogger()(嵌套上下文) | 19.10M | 52ns |
createRequestLogger() | 19.27M | 52ns |
上下文累积(log.set())
| 操作 | ops/sec | 平均值 |
|---|---|---|
| 浅层合并(3 个字段) | 9.54M | 105ns |
| 浅层合并(10 个字段) | 4.78M | 209ns |
| 深度嵌套合并 | 8.40M | 119ns |
| 4 次连续调用 | 7.53M | 133ns |
事件发射(log.emit())
| 操作 | ops/sec | 平均值 |
|---|---|---|
| 发射最小事件 | 1.75M | 570ns |
| 发射带上下文 | 1.76M | 569ns |
| 完整生命周期(创建 + 3 次 set + 发射) | 1.69M | 592ns |
| 发射带错误 | 66.1K | 15.13µs |
发射带错误 更慢,因为 Error.captureStackTrace() 是昂贵的 V8 操作(约 15µs)。仅当抛出错误时触发。负载扩展
| 负载 | ops/sec | 平均值 |
|---|---|---|
| 小(2 个字段) | 1.76M | 567ns |
| 中等(50 个字段) | 555.5K | 1.80µs |
| 大(200 个嵌套字段) | 115.7K | 8.65µs |
采样
| 操作 | ops/sec | 平均值 |
|---|---|---|
| 尾采样(shouldKeep) | 43.76M | 23ns |
| 完整发射(包含头部 + 尾部) | 7.57M | 132ns |
增强器
| 增强器 | ops/sec | 平均值 |
|---|---|---|
| 用户代理(Chrome) | 2.57M | 389ns |
| 地理定位(Vercel) | 5.32M | 188ns |
| 请求大小 | 24.16M | 41ns |
| 跟踪上下文 | 4.86M | 206ns |
| 全部组合 | 487.2K | 2.05µs |
错误处理
| 操作 | ops/sec | 平均值 |
|---|---|---|
createError() | 226.9K | 4.41µs |
parseError() | 43.92M | 23ns |
| 往返(创建 + 解析) | 227.6K | 4.39µs |
方法论与可信度
你能相信这些数字吗?
本页面上的每个基准测试都是开源的且可复现的。基准测试文件位于 packages/evlog/bench/。你可以阅读确切代码、在本机运行并验证结果。
所有库在相同条件下测试:
- 相同输出模式:JSON 输出到无操作目标(不测量磁盘或网络 I/O)
- 相同预热:每个基准测试在 JIT 稳定后运行 500ms
- 相同工具:Vitest bench,由 tinybench 提供支持
- 相同机器:比较库时,所有基准测试在同一进程的同一硬件上运行
CI 回归跟踪
性能回归在每次拉取请求时通过两个系统跟踪:
- CodSpeed 使用 CPU 指令计数运行所有基准测试(而非壁钟时间)。这消除了共享 CI 运行器中的噪声,并产生确定性、可复现的结果。回归直接在 PR 上标记。
- 打包大小比较 测量所有入口点与
main基线的对比,并在 PR 评论中发布大小差异报告。
自行运行
cd packages/evlog
bun run bench # 全部基准测试
bunx vitest bench bench/comparison/ # 仅与备选方案对比
bun bench/scripts/size.ts # 打包体积