点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群
来源 | https://nodesource.com/blog/State-of-Nodejs-Performance-2024
编译 | 公众号 Nodejs技术栈
2024 年,Node.js 已经达到了版本 23。由于每年发布两个主版本更新,可能很难跟踪 Node.js 的所有方面。本文将回顾 Node.js 性能状态,重点对比版本 20 到 22。目标是提供对过去一年平台演变的详细分析。
今年的报告延续了严谨基准测试的传统,提供了硬件细节和可重复的示例。为了简化体验,每个部分的开头都将折叠可重复的步骤,使读者能够轻松跟随而不分心。
本文仅对比 Node.js 版本,而不与其他 JavaScript 运行时进行对比。目的是突出平台的内部进展——性能的提升、回退及推动这些变化的因素。
基准测试设置
这篇博客将分享不同 Node 版本线的基准测试结果,使用两个仓库作为参考:
Node.js 内部基准测试套件
nodejs-bench-operations
使用 bench-node 作为基准测试工具。
基准测试在一台专用的 AWS 机器(C6i.xlarge)上运行,配置如下:
4 个 vCPU,8GB 内存
Ubuntu 22.04 LTS
使用的 Node.js 版本如下:
v20.17.0
v22.9.0
几个关键模块对 Node.js 性能有显著影响。任何这些核心组件的增强或回退都会在平台中产生共鸣。为了进行这次基准测试,我选择了以下核心模块:
assert - Node.js 断言操作
buffers - Node.js Buffer 操作
diagnostics_channel - Node.js 诊断通道模块
fs - Node.js 文件系统
path - UNIX 系统上的 Node.js 路径模块
streams - Node.js 流创建、销毁、可读等操作
misc - Node.js 启动时间,使用 child_process 和 worker_threads + trace_events
test_runner - Node.js 测试运行器
url - Node.js URL 解析器
util - Node.js 文本编码器/解码器
webstreams - Node.js WebStreams(根据 WHATWG 规范)
zlib - Node.js zlib API
所有基准测试结果都可以在 RafaelGSS/state-of-nodejs-performance-2024 中查看,同时也包括在专用机器上执行的基准测试脚本。
如何评估 Node.js 基准测试
如同在《Node.js 性能状态 2023》中所提到的,Node.js 基准测试套件默认会运行每个配置 30 次,以确保准确性,结果会通过学生 t 检验进行统计分析,t 检验衡量了每个基准测试的置信度。
三个星号 (***) 表示数据具有很高的置信度,具体如下面所示:
confidence improvement accuracy (*) (**) (***)
fs/readfile.js concurrent=1 len=16777216 encoding='ascii' duration=5 *** 67.59 % ±3.80% ±5.12% ±6.79%
fs/readfile.js concurrent=1 len=16777216 encoding='utf-8' duration=5 *** 11.97 % ±1.09% ±1.46% ±1.93%
fs/writefile-promises.js concurrent=1 size=1024 encodingType='utf' duration=5 0.36 % ±0.56% ±0.75% ±0.97%
Be aware that when doing many comparisons the risk of a false-positive result increases.
In this case, there are 10 comparisons, you can thus expect the following amount of false-positive results:
0.50 false positives, when considering a 5% risk acceptance (*, **, ***),
0.10 false positives, when considering a 1% risk acceptance (**, ***),
0.01 false positives, when considering a 0.1% risk acceptance (***)
性能更新与语义版本控制
许多性能改进作为 semver-minor 或 semver-patch 更新到达。虽然 Node.js v22.9.0 目前可能超越 Node.js v20.17.0,但随着时间推移,这种情况可能会发生变化,因为 v20 的 minor 和 patch 级别的改进会继续被回溯到旧版本。
为了解释这一点,以下是 Node.js v16、v18 和 v20 的提交比较。最新的提交,标记为黄色,不太可能会出现在 v16 中,因为它已进入维护模式。

与此同时,Node.js v20 中的这些最新提交很可能会被集成到 v18 中,因为 v18 处于长期支持(LTS)阶段,这意味着这些 v20 更新可以改善或潜在地降低 v18 的性能。
“注意:除非是处于“生命周期结束”(EOL)或“维护模式”下的发布线,否则跨版本线的结果应该谨慎看待。
为了通过数字来展示这个概念,我们来看一个在 2023 年报告中分享的场景:
Node.js v20.0.0 在事件处理方面,特别是使用 event.target
时,相对于 v18.16.0 展现了显著的提升,如下基准所示。这里,v20.0.0 处理的操作比 v18.16.0 多 200%,显示出一个主要的性能提升。

将其与 Node.js v22.9.0 相比,v22 相对于 v18.17.0 的提升约为 55%,这并不是因为 v22 更慢,而是因为 v18.17.0 收到了改进,缩小了 v18.16.0 与 v22 之间的性能差距。

v20.17.0 中的提交(如下所示)有效地将该性能差距从 200% 缩小至大约 55%,即 Node.js v18.17.0 的性能水平。

从哪里开始基准测试过程?
如果你是基准测试的新手,这篇博客文章是一个很好的起点。
准备环境: 准确基准测试的黄金法则是控制环境,因为几乎任何因素都可能影响结果。例如,在 Zoom 通话期间或播放音乐时进行基准测试可能会引入噪音干扰你的测量结果。举个著名的例子,2004 年,Brendan Gregg 演示了即使在硬件附近大声喊叫也能打断缓慢的磁盘 I/O 操作!
为了避免这种干扰,总是使用专用机器进行基准测试。
隔离瓶颈: 为了隔离瓶颈,你需要尽可能减少变量的影响。
基准测试工作流程:
使用专用机器运行基准测试。
在做出改变之前运行基准测试。
做出改变后再次运行相同的基准测试。
对比结果。
“注意:在 Node.js v22.9.0 之前,Maglev(V8 编译器)默认在 v22.x 版本线中启用。这个改变可能会导致如果你在不同版本线之间比较每秒操作次数时出现假阳性回归。Node.js v22.9.0 禁用了 Maglev,原因不同。因此,如果你在 Node.js v22.9.0 之前进行基准测试,可能会因为 Maglev 的影响而存在不准确的情况。参见:https://github.com/nodejs/performance/issues/166#issuecomment-2103317419
小心处理 JS 微基准测试
尽管许多微基准测试被创建并在网络上传播,但 JavaScript 中的微基准测试大多数时候(如果不是全部情况)并不能代表现实,并且在罕见的情况下是错误的。本文不会详细阐述为什么 JavaScript 微基准测试难以编写和评估,但重要的一点是要仔细阅读所有这些值(包括本文中分享的)。以下是一些阅读建议:
传统 JavaScript 基准测试的真相
JavaScript 基准测试 GOTO 2015
Node.js 内部基准测试
本节分享了通过运行 Node.js 内部基准测试套件获得的结果。尽管 Node.js 包含许多模块和数千个 API,本文将仅分享在基准测试过程中对性能产生显著影响的 API。因此,如果您最喜欢的 API 没有出现在此报告中,请假设从 v22.9.0 到 v20.17.0 之间没有性能变化。
Assert
node:assert
模块广泛用于 test_runner
和其他测试框架,因此使其更快很可能会使任何测试套件的运行速度更快。
assert.notDeepStrictEqual
现在在 Node.js v22 中比以前快 25%(对于小尺寸对象)。

assert.deepEqual(Buffers)
— 性能提升约 20%。

assert.strictEqual
— 由于可靠的样本量(n=200K),出现了约 7% 的性能下降。

Buffers
Node.js 在所有 Buffer
API 中的性能都有显著提升——除了处理 base64 数据时。
Buffer.byteLength
— 与 v20.17.0 比较,性能提升了 67%。

buffer.compare(buff)
— 性能提升超过 200%,这是一个显著的改进。

以下是所有加速的 Buffer
操作:
Buffer.concat()
- 提升 9% 至 33%!有效地将多个Buffer
合并为一个。Buffer.copy()
- 在使用Buffer.copy(buff, 0, buffLen)
进行缓冲区复制时,识别到 95% 的性能提升。Buffer.equals()
- 检查两个Buffer
是否具有相同的字节内容。某些结果达到了 150% 的提升(见下图)。Buffer.read*(0, byteLength)
- 从Buffer.readIntBE()
到Buffer.readUIntLE()
,性能显著提升,突破了 100% 的提升门槛。Buffer.slice()
- 在.slice()
操作中,Node.js v22.9.0 实现了 90% 的性能提升。Buffer.write(X, byteLength)
- 在.write()
中也获得了显著提升,从处理BigInt64BE
时的 5% 提升到处理FloatBE
时的 138% 提升。
总体而言,node:buffers
模块的性能非常出色,尽管 Buffer.isUTF8
和 Buffer.isASCII()
稍微出现了回归。

Diagnostics Channel
当没有订阅者时,diagnostic channels
的性能显著提升,最高提升了 120%,如图所示。这项改进对依赖诊断通道的用户尤为重要。在 NodeSource,我们在我们的 APM 中利用诊断通道,这项性能提升确保没有订阅者的系统不受影响。

Node.js 文件系统
Node.js 在 node:fs
模块中改善了错误场景的处理。例如,尝试打开不存在的文件失败的速度提高了约 58%。虽然这不会改变应用程序的功能,但它加快了那些定期检查文件可用性或完整性的进程的错误检测速度。

对于使用回调的 fs.opendir
,发现存在潜在的回归,因此在某些回调驱动的场景下,该函数的表现可能有所不同。

更快的 node:path
Node.js 的 node:path
模块也获得了性能提升。本次基准仅包括 POSIX 环境(Linux 和 macOS)。改进包括:
path.basename()
— 提升最高达 10%。

path.isAbsolute()
— 提升约 38%。

path.resolve()
— 在某些情况下提升约 9%。

node:streams
中的回归
在 node:streams
中,特别是在销毁流时,发现了明显的回归,性能下降幅度在 -20% 到 -36% 之间。

Node.js 测试运行器
Node.js 基准测试运行器在测试创建上显示出大约 10% 的性能提升。

并且并发测试在速度上有额外的 12% 提升。

Node.js URL 解析器
Node.js 的 URL 解析器变得更快了。URL.resolve
经过优化,带来了显著的性能提升。

TextDecode 回归
在 TextDecoder.decode()
中,尤其是对于 Latin-1 编码,出现了接近 100% 的性能下降。ISO8859-3 也受到类似影响。

然而,UTF-8 解码显示出 50% 的速度提升,为某些使用场景带来了显著改善。

WebStreams
WebStreams 在多个流类型(包括 Readable、Writable、Transform 和 Duplex)中都获得了显著的性能提升,某些情况下提升超过 100%。这对于 fetch
(一个广泛使用的 HTTP 请求工具)尤其重要,因为它依赖于 WebStreams 规范。

Fetch 与 WebStreams
fetch
API 是用于发起 HTTP 请求的 Web 标准,并且要求使用 WebStreams 作为其规范的一部分。因此,当 WebStreams 被优化时,fetch
也直接受益,这也是 WebStreams 性能提升如此重要的原因。

在 2022 年,发现了 undici 库(Node.js 使用的 fetch 实现)中的一个问题,导致 fetch
性能明显慢于其他替代方案。Rafael Gonzaga 提供了分析,解释了 WebStreams 固有的慢速是 fetch
性能受限的主要原因,因为 fetch
本身依赖 WebStreams 设计。
随着 Node.js v22 的发布,WebStreams 的改进帮助 fetch
从每秒 2,246 次请求提升到 2,689 次请求,标志着这个性能敏感的 API 取得了显著的改进。


Zlib 回归
Node.js 中的 zlib
模块提供了使用 Gzip 和 Deflate/Inflate 算法进行压缩和解压缩的功能。发现 zlib.deflate()
存在回归,尤其是异步 API(zlib.deflate()
)较同步调用(zlib.deflateSync()
)的影响更大。

使用 bench-node
避免微基准测试中的死代码消除
如在文章 "谨慎处理 JS 微基准测试" 中所述,基准测试通常是以某种方式编写的,经过 V8 优化后,代码会被移除,因为 V8 JIT 编译器会标记测量的代码为容易进行“死代码消除”。这会导致测量到的是一个 noop()
而不是预期的代码操作。

问题在于,一旦 V8 编译器优化了代码,它可能会完全丢弃基准测试代码,只留下一个无操作的函数。
为了解决这个问题,创建了 bench-node
库。默认情况下,bench-node
会指示 V8 永远不要优化基准测试代码,确保在基准测试过程中始终执行测量代码。
beforeClockTemplate(_varNames) {
let code = '';
code += `
function DoNotOptimize(x) {}
// Prevent DoNotOptimize from optimizing or being inlined.
%NeverOptimizeFunction(DoNotOptimize);
`
return [code, 'DoNotOptimize'];
}
本文不会深入讲解 bench-node
的内部实现。接下来的部分将展示使用该库生成的基准测试结果。虽然 bench-node
在提供可靠且一致的简单操作比较方面表现出色,但需要注意的是,这些结果可能无法反映真实的场景。在生产环境中,V8 的优化可能会显著影响性能,这使得完美复现运行时行为变得具有挑战性。
本文未包含但采用的其他基准测试方法
在进行这项研究时,采用了许多其他基准测试方法:
使用 tinybench 替代了 bench-node 来验证
nodejs-bench-operations
结果的准确性。还进行了使用 wrk2 和不同 HTTP 框架(如 express 和 fastify)的 HTTP 基准测试,但未发现值得在本文中提及的显著差异。
还使用了 NodeSource/nodejs-package-benchmark,这是一个用于常见 Web 开发者工作负载的 Node.js 基准测试,但没有发现显著的结果。
为什么会出现回归?Node.js 团队难道不会为每个 PR 测量回归吗?
要获得上述基准测试结果,需要一台专用的机器来运行整个 Node.js 测试套件,这个过程花费了四天时间。试想一下,如果对 Node.js 核心代码进行了一个小的修改,可能直到运行基准测试后才能知道是否引入了回归。如果每个拉取请求都运行完整的基准测试,且每次测试都需要数天时间,这将极为消耗资源,并且会显著拖慢开发进度。
考虑到 Node.js 项目的规模——成千上万的贡献者和庞大的代码库——跟踪每一个潜在的回归是非常具有挑战性的。团队力求在全面测试与实际资源限制之间找到平衡,确保关键领域得到充分覆盖,同时优先考虑快速开发。
尽管如此,我们始终在积极监控性能,并且欢迎赞助项目,这将有助于扩展我们的基准测试能力,帮助我们更早发现回归,并进一步提高发布质量。
致谢
这篇文章的完成,得益于 NodeSource 对 Rafael 工作的支持,以及提供了专用机器来运行所有基准测试。
- End -
Node 社群
我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。
“分享、点赞、在看” 支持一波👍