2023年前端开发者必备:Node.js综合性能指南

发表时间: 2023-05-27 22:18

家好,很高兴又见面了,我是"高级前端‬进阶‬",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

高级前端‬进阶

今天给大家带来的是 blog.rafaelgss.dev 平台发布的一篇《State of Node.js Performance 2023》。觉得特别有意思,就翻译了过来,给大家一起看看。话不多说,直接进入正题。

前言

今年是 2023 年,Node.js 官方发布了v20,这是一项重大变更,本文旨在使用科学数字来评估 Node.js 的性能状态。

所有基准测试结果都包含可重现的示例和硬件详细信息, 为了减少普通读者的噪音,可重现的步骤将折叠在所有部分的开头。

本文旨在对不同版本的 Node.js 进行对比分析,同时突出改进和不足,并提供了对不同变更的看法。不过,值得一提的是,本文没有与其他 JavaScript 运行时进行任何比较。

为了进行科学实验,本文使用了 Node.js 版本 16.20.0、18.16.0 和 20.0.0,并将基准套件分为三个不同的组:

Node.js 内部基准

鉴于 Node.js 基准测试套件的巨大规模和耗时特性,本文选择了对 Node.js 开发人员和配置影响更大的基准测试,例如:使用 fs 读取 16 MB 的文件 .readfile。 这些基准测试按模块分组,例如 fs 和 streams。

nodejs-bench-operations

来源于一个名为 nodejs-bench-operations 的存储库,其中包括所有 Node.js 主要版本的基准操作,以及每个版本系列的最后三个版本。

这样可以轻松比较不同版本之间的结果,例如: Node.js v16.20.0 和 v18.16.0,或 v19.8.0 和 v19.9.0,目的是识别 Node.js 代码库中的回归。

HTTP 服务器(框架)

这个实用的 HTTP 基准测试向各种路由发送大量请求,返回 JSON、纯文本和错误,以 express 和 fastify 为参考。 主要目的是确定从 Node.js 内部基准测试和 nodejs-bench-operations 获得的结果是否适用于常见的 HTTP 应用程序。

1.环境

为执行此基准测试,AWS 专用主机与以下计算优化实例一起使用:

  • c6i.xlarge (Ice Lake) 3,5 GHz - Computing Optimized
  • 4 vCPUs
  • 8 GB Mem
  • Canonical, Ubuntu, 22.04 LTS, amd64 jammy
  • 1GiB SSD Volume Type

Node.js 内部基准

在此基准测试中选择了以下模块/命名空间:

  • fs : Node.js 文件系统
  • 事件 :Node.js 事件类 EventEmitter / EventTarget
  • http :Node.js HTTP 服务器 + 解析器
  • misc :使用 child_processes 和 worker_threads + trace_events 的 Node.js 启动时间
  • module : Node.js module.require
  • streams :Node.js 流创建、销毁、可读等
  • url :Node.js URL 解析器
  • 缓冲区 :Node.js 缓冲区操作
  • util : Node.js 文本编码器/解码器

使用的配置可在 RafaelGSS/node#state-of-nodejs (
https://github.com/RafaelGSS/node/tree/state-of-nodejs)获得,所有结果都发布在主存储库中:State of Node.js Performance 2023(
https://github.com/RafaelGSS/state-of-nodejs-performance-2023)。

2.Node.js 基准测试方法

在展示结果之前,解释用于确定基准结果置信度的统计方法至关重要。 这个方法在之前的一篇博文中有详细的解释,可以参考这里:准备和评估基准(
https://blog.rafaelgss.dev/preparing-and-evaluating-benchmarks)。

为了比较新 Node.js 版本的影响,在每个配置和 Node.js 16、18 和 20 上多次运行每个基准测试 (30)。当输出显示为表格时,有两列需要特别注意:

  • improvement :相对于新版本的改进百分比
  • confidence(置信度): 表示是否有足够的统计证据来验证改进

例如,考虑下表结果:

                                                                              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 (***)

有 0.1% 的风险 fs.readfile 没有从 Node.js 16 改进到 Node.js 18 中受益。 因此,可以认为对数据结果非常有信心。 表结构可以理解为:

  • fs/readfile.js :基准文件
  • concurrent=1 len=16777216 encoding='ascii' duration=5 : 基准选项, 每个基准文件可以有很多选项,在这种情况下,它使用 ASCII 作为编码方法在 5 秒内读取 1 个 16777216 字节的并发文件。

基准设置

  • 克隆 fork Node.js 仓库
  • 切换到 state-of-nodejs 分支
  • 创建 Node.js 16、18 和 20 二进制文件
  • 运行 benchmark.sh 脚本

//1git clone git@github.com:RafaelGSS/node.git//2cd node && git checkout state-of-nodejs//3nvm install v20.0.0cp $(which node) ./node20nvm install v18.16.0cp $(which node) ./node18nvm install v16.20.0cp $(which node) ./node16//4./benchmark.sh

文件系统

将 Node.js 从 16 升级到 18 时,使用带有 ascii 编码的 fs.readfile API 时观察到 67% 的性能改进,使用 utf-8 时观察到大约 12% 的改进。

基准测试结果表明,将 Node.js 从版本 16 升级到 18 时,使用 ascii 编码的 fs.readfile API 提高了大约 67%,使用 utf-8 时提高了大约 12%。用于基准测试的文件使用以下代码片段创建:

const data = Buffer.alloc(16 * 1024 * 1024, 'x');fs.writeFileSync(filename, data);

但是,在 Node.js 20 上使用带有 ascii 的 fs.readfile 时出现了 27% 的性能回归。 此回归已报告给 Node.js 性能团队,预计会很快得到修复。 另一方面,fs.opendir、fs.realpath 和 fs.readdir 都显示了从 Node.js 18 到 Node.js 20 的改进。Node.js 18 和 20 之间的比较可以在下面的基准测试结果中看到:

                                                                              confidence improvement accuracy (*)   (**)  (***)fs/bench-opendir.js bufferSize=1024 mode='async' dir='test/parallel' n=100           ***      3.48 %       ±0.22% ±0.30% ±0.39%fs/bench-opendir.js bufferSize=32 mode='async' dir='test/parallel' n=100             ***      7.86 %       ±0.29% ±0.39% ±0.50%fs/bench-readdir.js withFileTypes='false' dir='test/parallel' n=10                   ***      8.69 %       ±0.22% ±0.30% ±0.39%fs/bench-realpath.js pathType='relative' n=10000                                     ***      5.13 %       ±0.97% ±1.29% ±1.69%fs/readfile.js concurrent=1 len=16777216 encoding='ascii' duration=5                 ***    -27.30 %       ±4.27% ±5.75% ±7.63%fs/readfile.js concurrent=1 len=16777216 encoding='utf-8' duration=5                 ***      3.25 %       ±0.61% ±0.81% ±1.06%  0.10 false positives, when considering a   5% risk acceptance (*, **, ***),  0.02 false positives, when considering a   1% risk acceptance (**, ***),  0.00 false positives, when considering a 0.1% risk acceptance (***)

如果使用的是 Node.js 16,则可以使用以下 Node.js 16 和 Node.js 20 之间的比较:

                                                                              confidence improvement accuracy (*)    (**)   (***)fs/bench-opendir.js bufferSize=1024 mode='async' dir='test/parallel' n=100           ***      2.79 %       ±0.26%  ±0.35%  ±0.46%fs/bench-opendir.js bufferSize=32 mode='async' dir='test/parallel' n=100             ***      5.41 %       ±0.27%  ±0.35%  ±0.46%fs/bench-readdir.js withFileTypes='false' dir='test/parallel' n=10                   ***      2.19 %       ±0.26%  ±0.35%  ±0.45%fs/bench-realpath.js pathType='relative' n=10000                                     ***      6.86 %       ±0.94%  ±1.26%  ±1.64%fs/readfile.js concurrent=1 len=16777216 encoding='ascii' duration=5                 ***     21.96 %       ±7.96% ±10.63% ±13.92%fs/readfile.js concurrent=1 len=16777216 encoding='utf-8' duration=5                 ***     15.55 %       ±1.09%  ±1.46%  ±1.92%

事件

EventTarget 类在事件方面表现出最显著的改进。 该基准涉及使用
EventTarget.prototype.dispatchEvent(new Event('foo')) 分派一百万个事件。

从 Node.js 16 升级到 Node.js 18 可以使事件调度性能提高近 15%。 但真正的飞跃出现在从 Node.js 18 升级到 Node.js 20 时,当只有一个侦听器时,它可以产生高达 200% 的性能提升。

EventTarget 类是 Web API 的重要组成部分,用于各种父功能,例如: AbortSignal 和 worker_threads。 因此,对此类进行的优化可能会影响这些功能的性能,包括:获取和 AbortController。 此外,
EventEmitter.prototype.emit API 在 Node.js 16 和 Node.js 20 上也有显著提升,大约有 11.5% 的提升。下面提供一个综合比较,供大家参考:

                                                                 confidence improvement accuracy (*)   (**)  (***)events/ee-emit.js listeners=5 argc=2 n=2000000                          ***     11.49 %       ±1.37% ±1.83% ±2.38%events/ee-once.js argc=0 n=20000000                                     ***     -4.35 %       ±0.47% ±0.62% ±0.81%events/eventtarget-add-remove.js nListener=10 n=1000000                 ***      3.80 %       ±0.83% ±1.11% ±1.46%events/eventtarget-add-remove.js nListener=5 n=1000000                  ***      6.41 %       ±1.54% ±2.05% ±2.67%events/eventtarget.js listeners=1 n=1000000                             ***    259.34 %       ±2.83% ±3.81% ±5.05%events/eventtarget.js listeners=10 n=1000000                            ***    176.98 %       ±1.97% ±2.65% ±3.52%events/eventtarget.js listeners=5 n=1000000                             ***    219.14 %       ±2.20% ±2.97% ±3.94%

HTTP

HTTP 服务器是 Node.js 中最具影响力的改进层之一,现在大多数 Node.js 应用程序都运行 HTTP 服务器。 因此,任何更改都可以很容易地被视为主要更改,并为性能的带来变化。

因此,使用的 HTTP 服务器是一个 http.Server,它在每个请求中回复 4 个 256 字节的块,每个块包含“C”,比如下面的例子:

http.createServer((req, res) => {    const n_chunks = 4;    const body = 'C'.repeat();    const len = body.length;		res.writeHead(200, {				'Content-Type': 'text/plain',		    'Content-Length': len.toString()		});    for (i = 0, n = (n_chunks - 1); i < n; ++i)      res.write(body.slice(i * step, i * step + step));    res.end(body.slice((n_chunks - 1) * step));})// See: https://github.com/nodejs/node/blob/main/benchmark/fixtures/simple-http-server.js

比较 Node.js 16 和 Node.js 18 的性能时,有 8% 的显著提升。然而,从 Node.js 18 升级到 Node.js 20 带来了 96.13% 的显著改进。

这些基准测试结果是使用 test-double-http 基准测试方法收集的(
https://github.com/nodejs/node/blob/main/benchmark/_test-double-benchmarker.js),即发送 HTTP GET 请求的简单 Node.js 脚本:

function run() {  if (http.get) { // HTTP or HTTPS    if (options) {      http.get(url, options, request);    } else {      http.get(url, request);    }  } else { // HTTP/2    const client = http.connect(url);    client.on('error', () => {});    request(client.request(), client);  }}run();

通过切换到更可靠的基准测试工具,如 autocannon 或 wrk,观察到报告的性能改进显著下降,比如从 96% 到 9%。 这表明以前的基准测试方法存在局限性或错误。 然而,HTTP 服务器的实际性能有所提高,需要使用新的基准测试方法仔细评估改进的百分比,以准确评估取得的进展。

那么开发者可以期望 Express/Fastify 应用程序有 96%/9% 的性能提升吗?绝对不, 框架可能会选择不使用内部 HTTP API,这也是 Fastify 如此快的原因之一。

Misc

根据测试,startup.js 脚本展示了 Node.js 进程生命周期的显著改进,从 Node.js 18 版到 20 版观察到 27% 的提升。与 Node.js 相比,这种改进更加令人印象深刻,比如 Node.js 16版本,启动时间减少了 34.75%!

随着现代应用程序越来越依赖于无服务器系统,减少启动时间已成为提高整体性能的关键因素。 值得注意的是,Node.js 团队一直致力于优化平台的这一方面,战略计划证明了这一点:
https://github.com/nodejs/node/issues/35711。

这些启动时间的改进不仅有利于无服务器应用程序,而且还增强了依赖快速启动时间的其他 Node.js 应用程序的性能。 总的来说,这些更新表明了 Node.js 团队致力于为所有用户提高平台的速度和效率。

$ node-benchmark-compare compare-misc-16-18.csv                                                                                     confidence improvement accuracy (*)   (**)  (***)misc/startup.js count=30 mode='process' script='benchmark/fixtures/require-builtins'        ***     12.99 %       ±0.14% ±0.19% ±0.25%misc/startup.js count=30 mode='process' script='test/fixtures/semicolon'                    ***      5.88 %       ±0.15% ±0.20% ±0.26%misc/startup.js count=30 mode='worker' script='benchmark/fixtures/require-builtins'         ***      5.26 %       ±0.14% ±0.19% ±0.25%misc/startup.js count=30 mode='worker' script='test/fixtures/semicolon'                     ***      3.84 %       ±0.15% ±0.21% ±0.27%$ node-benchmark-compare compare-misc-18-20.csv                                                                                     confidence improvement accuracy (*)   (**)  (***)misc/startup.js count=30 mode='process' script='benchmark/fixtures/require-builtins'        ***     -4.80 %       ±0.13% ±0.18% ±0.23%misc/startup.js count=30 mode='process' script='test/fixtures/semicolon'                    ***     27.27 %       ±0.22% ±0.29% ±0.38%misc/startup.js count=30 mode='worker' script='benchmark/fixtures/require-builtins'         ***      7.23 %       ±0.21% ±0.28% ±0.37%misc/startup.js count=30 mode='worker' script='test/fixtures/semicolon'                     ***     31.26 %       ±0.33% ±0.44% ±0.58%

这个基准非常简单,即测量使用给定的 [script] 创建新 [mode] 时经过的时间,其中 [mode] 可以是:

  • process - 一个新的 Node.js 进程
  • worker - 一个 Node.js worker_thread

而【脚本】又分为:

  • benchmark/fixtures/require-builtins :一个需要所有 Node.js 模块的脚本
  • test/fixtures/semicolon : 一个空脚本,包含一个 ; (分号)

这个实验可以很容易地用hyperfine或time重现:

$ hyperfine --warmup 3 './node16 ./nodejs-internal-benchmark/semicolon.js'Benchmark 1: ./node16 ./nodejs-internal-benchmark/semicolon.js  Time (mean ± σ):      24.7 ms ±   0.3 ms    [User: 19.7 ms, System: 5.2 ms]  Range (min  max):    24.1 ms   25.6 ms    121 runs$ hyperfine --warmup 3 './node18 ./nodejs-internal-benchmark/semicolon.js'Benchmark 1: ./node18 ./nodejs-internal-benchmark/semicolon.js  Time (mean ± σ):      24.1 ms ±   0.3 ms    [User: 18.1 ms, System: 6.3 ms]  Range (min  max):    23.6 ms   25.3 ms    123 runs$ hyperfine --warmup 3 './node20 ./nodejs-internal-benchmark/semicolon.js'Benchmark 1: ./node20 ./nodejs-internal-benchmark/semicolon.js  Time (mean ± σ):      18.4 ms ±   0.3 ms    [User: 13.0 ms, System: 5.9 ms]  Range (min  max):    18.0 ms   19.7 ms    160 runs

trace_events 模块也经历了显著的性能提升,在将 Node.js 版本 16 与版本 20 进行比较时观察到 7% 的改进。值得注意的是,在将 Node.js 版本 18 与版本 20 进行比较时,此改进略低,为 2.39% 。

模块

require()(或 module.require)长期以来一直是 Node.js 启动时间缓慢的罪魁祸首。 但是,最近的性能改进表明此功能也已得到优化。 在 Node.js 版本 18 和 20 之间,观察到需要 .js 文件时改进了 4.20%,.json 文件时改进了 6.58%,读取目录时改进了 9.50% 。 所有这些都有助于加快启动时间。

优化 require() 至关重要,因为它是 Node.js 应用程序中大量使用的函数。 通过减少执行此功能所需的时间,可以显著加快整个启动过程并改善用户体验

stream

流是 Node.js 的一个非常强大且广泛使用的特性。 但是,在 Node.js 版本 16 和 18 之间,一些与流相关的操作变慢了。 这包括创建和销毁 Duplex、Readable、Transform 和 Writable 流,以及 Readable → Writable 流的 .pipe() 方法。

下图说明了这种回归:

然而,这种 pipe 回归在 Node.js 20 中减少了:

$ node-benchmark-compare compare-streams-18-20.csv                                                       confidence improvement accuracy (*)   (**)  (***)streams/creation.js kind='duplex' n=50000000                  ***     12.76 %       ±4.30% ±5.73% ±7.47%streams/creation.js kind='readable' n=50000000                ***      3.48 %       ±1.16% ±1.55% ±2.05%streams/creation.js kind='transform' n=50000000                **     -7.59 %       ±5.27% ±7.02% ±9.16%streams/creation.js kind='writable' n=50000000                ***      4.20 %       ±0.87% ±1.16% ±1.53%streams/destroy.js kind='duplex' n=1000000                    ***     -6.33 %       ±1.08% ±1.43% ±1.87%streams/destroy.js kind='readable' n=1000000                  ***     -1.94 %       ±0.70% ±0.93% ±1.21%streams/destroy.js kind='transform' n=1000000                 ***     -7.44 %       ±0.93% ±1.24% ±1.62%streams/destroy.js kind='writable' n=1000000                           0.20 %       ±1.89% ±2.52% ±3.29%streams/pipe.js n=5000000                                     ***     87.18 %       ±2.58% ±3.46% ±4.56%

正如可能已经注意到的,某些类型的流(特别是 Transform)在 Node.js 20 中退化了。因此,Node.js 16 仍然拥有最快的流。对于这个特定的基准测试,请不要将这个基准测试结果解读为“Node v18 和 v20 中的 .js 流太慢了!”这是一个特定的基准测试,可能会也可能不会影响实际的工作负载。 例如,如果查看 nodejs-bench-operations 中的简单比较,您会发现以下代码片段在 Node.js 20 上的性能优于其前身:

suite.add('streams.Writable writing 1e3 * "some data"', function () {  const writable = new Writable({    write (chunk, enc, cb) {      cb()    }  })  let i = 0  while(i < 1e3) {    writable.write('some data')    ++i  }})

事实上,实例化和销毁方法在 Node.js 生态系统中扮演着重要的角色。 因此,它很可能会对某些库产生负面影响。 但是,Node.js 性能工作组正在密切监视这种回归。

请注意,可读异步迭代器在 Node.js 20 上变得稍快(~6.14%)。

URL

从 Node.js 18 开始,Node.js 添加了一个新的 URL 解析器依赖,Ada。 此添加将解析 URL 时的 Node.js 性能提升到一个新的水平, 一些结果可以达到 400% 的改进。 作为普通用户,您可能不会直接使用它。 但是,如果使用 HTTP 服务器,那么它很可能会受到此性能改进的影响。

URL 基准套件相当大。 因此,将仅涵盖 WHATWG URL 基准测试结果。

url.parse() 和 url.resolve() 都是已弃用的遗留 API。 尽管它的使用被认为对任何 Node.js 应用程序都有风险,但开发人员仍在使用它。 引用 Node.js 文档:

url.parse() 使用一种宽松的、非标准的算法来解析 URL 字符串。 它很容易出现安全问题,例如主机名欺骗和用户名和密码的不正确处理。 不要使用不受信任的输入。 不会针对 url.parse() 漏洞发布 CVE。 请改用 WHATWG URL API。

如果对 url.parse 和 url.resolve 的性能变化感到好奇,请查看 State of Node.js Performance 2023 存储库(
https://github.com/RafaelGSS/state-of-nodejs-performance-2023#url-results),新的 whatwg-url-parse 的结果真的很有趣:

下面是用于基准测试的 URL 列表,这些 URL 是根据基准配置选择的:

const urls = {  long: 'http://nodejs.org:89/docs/latest/api/foo/bar/qua/13949281/0f28b/' +        '/5d49/b3020/url.html#test?payload1=true&payload2=false&test=1' +        '&benchmark=3&foo=38.38.011.293&bar=1234834910480&test=19299&3992&' +        'key=f5c65e1e98fe07e648249ad41e1cfdb0',  short: 'https://nodejs.org/en/blog/',  idn: 'http://你好你好.在线',  auth: 'https://user:pass@example.com/path?search=1',  file: 'file:///foo/bar/test/node.js',  ws: 'ws://localhost:9229/f46db715-70df-43ad-a359-7f9949f39868',  javascript: 'javascript:alert("node is awesome");',  percent: 'https://%E4%BD%A0/foo',  dot: 'https://example.org/./a/../b/./c',}

随着最近 Node.js 20 中 Ada 2.0 的升级,可以说 Node.js 18 与 Node.js 20 相比也有了显著的改进:

基准文件非常简单:

function useWHATWGWithoutBase(data) {  const len = data.length;  let result = new URL(data[0]);  // Avoid dead code elimination  bench.start();  for (let i = 0; i < len; ++i) {    result = new URL(data[i]);  }  bench.end(len);  return result;}function useWHATWGWithBase(data) {  const len = data.length;  let result = new URL(data[0][0], data[0][1]);  // Avoid dead code elimination  bench.start();  for (let i = 0; i < len; ++i) {    const item = data[i];    result = new URL(item[0], item[1]);  }  bench.end(len);  return result;}

唯一的区别是在创建/解析 URL 时用作基础的第二个参数。 还值得一提的是,当传递基数时(withBase='true'),它往往比常规使用(新 URL(数据))执行得更快。

Buffer

在 Node.js 中,缓冲区用于处理二进制数据。 缓冲区是一种内置数据结构,可用于将原始二进制数据存储在内存中,这在处理网络协议、文件系统操作或其他低级操作时非常有用。 总体而言,缓冲区是 Node.js 的重要组成部分,并且在整个平台中广泛用于处理二进制数据。

对于那些直接或间接使用 Node.js 缓冲区的人,除了提高 Buffer.from() 的性能之外,Node.js 20 还修复了 Node.js 18 的两个主要回归:

  • Buffer.concat()

Node.js 版本 20 与版本 18 相比有了显著改进,即使与版本 16 相比,这些改进仍然很明显:

  • Buffer.toJSON()

从 Node.js 16 到 Node.js 18,观察到 Buffer.toJSON 的性能下降了 88%:

$ node-benchmark-compare compare-buffers-16-18.csv                                                                            confidence improvement accuracy (*)    (**)   (***)buffers/buffer-tojson.js len=256 n=10000                                           ***    -81.12 %       ±1.25%  ±1.69%  ±2.24%buffers/buffer-tojson.js len=4096 n=10000                                          ***    -88.39 %       ±0.69%  ±0.93%  ±1.23%

然而,这种回归在 Node.js 20 中得到了修复和改进了几个数量级!

$ node-benchmark-compare compare-buffers-18-20.csv                                                                            confidence improvement accuracy (*)    (**)   (***)buffers/buffer-tojson.js len=256 n=10000                                           ***    482.81 %       ±7.02% ±9.42% ±12.42%buffers/buffer-tojson.js len=4096 n=10000             

因此,说 Node.js 20 是处理缓冲区最快的 Node.js 版本是正确的。 请参阅下面的 Node.js 20 和 Node.js 18 之间的完整比较:

$ node-benchmark-compare compare-buffers-18-20.csv                                                                            confidence improvement accuracy (*)   (**)   (***)buffers/buffer-base64-decode.js size=8388608 n=32                                  ***      1.66 %       ±0.10% ±0.14%  ±0.18%buffers/buffer-base64-encode.js n=32 len=67108864                                  ***     -0.44 %       ±0.17% ±0.23%  ±0.30%buffers/buffer-compare.js n=1000000 size=16                                        ***     -3.14 %       ±0.82% ±1.09%  ±1.41%buffers/buffer-compare.js n=1000000 size=16386                                     ***    -15.56 %       ±5.97% ±7.95% ±10.35%buffers/buffer-compare.js n=1000000 size=4096                                              -2.63 %       ±3.09% ±4.11%  ±5.35%buffers/buffer-compare.js n=1000000 size=512                                       ***     -6.15 %       ±1.28% ±1.71%  ±2.24%buffers/buffer-concat.js n=800000 withTotalLength=0 pieceSize=1 pieces=16          ***    300.67 %       ±0.71% ±0.95%  ±1.24%buffers/buffer-concat.js n=800000 withTotalLength=0 pieceSize=1 pieces=4           ***    212.56 %       ±4.81% ±6.47%  ±8.58%buffers/buffer-concat.js n=800000 withTotalLength=0 pieceSize=16 pieces=16         ***    287.63 %       ±2.47% ±3.32%  ±4.40%buffers/buffer-concat.js n=800000 withTotalLength=0 pieceSize=16 pieces=4          ***    216.54 %       ±1.24% ±1.66%  ±2.17%buffers/buffer-concat.js n=800000 withTotalLength=0 pieceSize=256 pieces=16        ***     38.44 %       ±1.04% ±1.38%  ±1.80%buffers/buffer-concat.js n=800000 withTotalLength=0 pieceSize=256 pieces=4         ***     91.52 %       ±3.26% ±4.38%  ±5.80%buffers/buffer-concat.js n=800000 withTotalLength=1 pieceSize=1 pieces=16          ***    192.63 %       ±0.56% ±0.74%  ±0.97%buffers/buffer-concat.js n=800000 withTotalLength=1 pieceSize=1 pieces=4           ***    157.80 %       ±1.52% ±2.02%  ±2.64%buffers/buffer-concat.js n=800000 withTotalLength=1 pieceSize=16 pieces=16         ***    188.71 %       ±2.33% ±3.12%  ±4.10%buffers/buffer-concat.js n=800000 withTotalLength=1 pieceSize=16 pieces=4          ***    151.18 %       ±1.13% ±1.50%  ±1.96%buffers/buffer-concat.js n=800000 withTotalLength=1 pieceSize=256 pieces=16        ***     20.83 %       ±1.29% ±1.72%  ±2.25%buffers/buffer-concat.js n=800000 withTotalLength=1 pieceSize=256 pieces=4         ***     59.13 %       ±3.18% ±4.28%  ±5.65%buffers/buffer-from.js n=800000 len=100 source='array'                             ***      3.91 %       ±0.50% ±0.66%  ±0.87%buffers/buffer-from.js n=800000 len=100 source='arraybuffer-middle'                ***     11.94 %       ±0.65% ±0.86%  ±1.13%buffers/buffer-from.js n=800000 len=100 source='arraybuffer'                       ***     12.49 %       ±0.77% ±1.03%  ±1.36%buffers/buffer-from.js n=800000 len=100 source='buffer'                            ***      7.46 %       ±1.21% ±1.62%  ±2.12%buffers/buffer-from.js n=800000 len=100 source='object'                            ***     12.70 %       ±0.84% ±1.12%  ±1.47%buffers/buffer-from.js n=800000 len=100 source='string-base64'                     ***      2.91 %       ±1.40% ±1.88%  ±2.46%buffers/buffer-from.js n=800000 len=100 source='string-utf8'                       ***     12.97 %       ±0.77% ±1.02%  ±1.33%buffers/buffer-from.js n=800000 len=100 source='string'                            ***     16.61 %       ±0.71% ±0.95%  ±1.25%buffers/buffer-from.js n=800000 len=100 source='uint16array'                       ***      5.64 %       ±0.84% ±1.13%  ±1.48%buffers/buffer-from.js n=800000 len=100 source='uint8array'                        ***      6.75 %       ±0.95% ±1.28%  ±1.68%buffers/buffer-from.js n=800000 len=2048 source='array'                                     0.03 %       ±0.33% ±0.43%  ±0.56%buffers/buffer-from.js n=800000 len=2048 source='arraybuffer-middle'               ***     11.73 %       ±0.55% ±0.74%  ±0.96%buffers/buffer-from.js n=800000 len=2048 source='arraybuffer'                      ***     12.85 %       ±0.55% ±0.73%  ±0.96%buffers/buffer-from.js n=800000 len=2048 source='buffer'                           ***      7.66 %       ±1.28% ±1.70%  ±2.21%buffers/buffer-from.js n=800000 len=2048 source='object'                           ***     11.96 %       ±0.90% ±1.20%  ±1.57%buffers/buffer-from.js n=800000 len=2048 source='string-base64'                    ***      4.10 %       ±0.46% ±0.61%  ±0.79%buffers/buffer-from.js n=800000 len=2048 source='string-utf8'                      ***     -1.30 %       ±0.71% ±0.96%  ±1.27%buffers/buffer-from.js n=800000 len=2048 source='string'                           ***     -2.23 %       ±0.93% ±1.25%  ±1.64%buffers/buffer-from.js n=800000 len=2048 source='uint16array'                      ***      6.89 %       ±1.44% ±1.91%  ±2.49%buffers/buffer-from.js n=800000 len=2048 source='uint8array'                       ***      7.74 %       ±1.36% ±1.81%  ±2.37%buffers/buffer-tojson.js len=0 n=10000                                             ***    -11.63 %       ±2.34% ±3.11%  ±4.06%buffers/buffer-tojson.js len=256 n=10000                                           ***    482.81 %       ±7.02% ±9.42% ±12.42%buffers/buffer-tojson.js len=4096 n=10000                                          ***    763.34 %       ±5.22% ±7.04%  ±9.34%

文本编解码

TextDecoder 和 TextEncoder 是两个 JavaScript 类,它们是 Web API 规范的一部分,可在现代 Web 浏览器和 Node.js 中使用。 TextDecoder 和 TextEncoder 一起提供了一种在 JavaScript 中处理文本数据的简单而有效的方法,允许开发人员执行涉及字符串和字符编码的各种操作。

解码和编码变得比 Node.js 18 快得多。通过添加用于 UTF-8 解析观察到的基准的 simdutf,与 Node.js 16 相比,解码时的结果提高了 364%(质的飞跃)。

这些改进在 Node.js 20 上变得更好,与 Node.js 18 相比性能提高了 25%。在
state-of-nodejs-performance-2023 存储库(
https://github.com/RafaelGSS/state-of-nodejs-performance-2023#util)中查看完整结果。

在 Node.js 18 上比较编码方法时也观察到性能改进。从 Node.js 16 到 Node.js 18,TextEncoder.encodeInto 在当前观察中达到了 93.67% 的改进(使用字符串长度为 256 的 ascii):

3.Node.js 基准操作

Node.js 中的基准测试操作很有意思,作为一个喜欢探索 Node.js 及其底层技术的人,我发现深入研究这些操作的细节非常有趣,尤其是那些与 V8 引擎相关的操作。

此外,这些基准测试将使用 ops/sec 指标,这基本上表示一秒钟内执行的操作数。 需要强调的是,这仅意味着计算时间的一小部分。

解析整数

可以使用 + 或 parseInt(x, 10) 将字符串解析为数字。之前的基准测试结果表明,在早期版本的 Node.js 中使用 + 比 parseInt(x, 10) 更快,如下表所示:

然而,随着 Node.js 20 和新的 V8 版本 (11.4) 的发布,这两种操作在性能方面变得相当,如下更新的基准测试结果所示:

super vs this

随着 Node.js 20 发布而发生变化的有趣基准之一是类中 this 或 super 的使用,如下面的示例:

class Base {  foo () {    return 10 * 1e2  }}class SuperClass extends Base {  bar () {    const tmp = 20 * 23    return super.foo() + tmp  }}class ThisClass extends Base {  bar () {    const tmp = 20 * 23    return this.foo() + tmp  }}

Node.js 18 中 super 和 this 之间的比较是每秒产生以下操作 (ops/sec):


这两种方法与 Node.js 20 之间没有显著差异,此声明略有不同:

根据基准测试结果,与 Node.js 18 相比,在 Node.js 20 上使用时,性能有了提高,而且非常明显。在 Node 上实现了 853,619,840 次操作/秒。

Nodejs 20 与 Node.js 18 上的每秒 160,092,440 次操作相比,性能提高了 433%! 显然,它具有与常规对象相同的属性访问方法:obj.property1。 另请注意,这两个操作均在相同的专用环境中进行了测试。 因此,这不太可能是偶然发生的。

属性访问

在 JavaScript 中有多种向对象添加属性的方法。 作为开发人员,可能想知道每种方法中属性访问的效率。

好消息是 nodejs-bench-operations 存储库包含对这些方法的比较,这揭示了它们的性能特征。 事实上,该基准测试数据表明 Node.js 20 中的属性访问已经有了显著改进,尤其是在使用具有writtable:true 和/enumerable/configurable: false 属性的对象时。

const myObj = {};Object.defineProperty(myObj, 'test', {  writable: true,  value: 'Hello',  enumerable: false,  configurable: false,});myObj.test // How fast is the property access?

在 Node.js 18 上,属性访问 (myObj.test) 每秒产生 166,422,265 次操作。 然而,在相同的情况下,Node.js 20 每秒产生 857,316,403 次操作!

Array.prototype.at

Array.prototype.at(-1) 是 ECMAScript 2021 规范中引入的一种方法。 它允许在不知道数组长度或使用负索引的情况下访问数组的最后一个元素,这在某些用例中可能是一个有用的特性。 通过这种方式,与 array[array.length - 1] 等传统方法相比,at() 方法提供了一种更简洁和可读的方式来访问数组的最后一个元素。

在 Node.js 18 上,与 Array[length-1] 相比,此访问速度相当慢:

从 Node.js 19 开始,Array.prototype.at 相当于老式的 Array[length-1] 如下表所示:

String.prototype.includes

大多数人都知道 RegExp 经常是任何类型应用程序中许多瓶颈的根源。 例如,可能想检查某个变量是否包含 application/json。虽然可以通过多种方式来执行此操作,但大多数情况下您最终会使用以下任一方法:

/application\/json/.test(text) - 正则表达式

或者

text.includes('application/json') - String.prototype.includes

有些人可能不知道 String.prototype.includes 几乎和 Node.js 16 上的 RegExp 一样慢。

但是,自 Node.js 18 起,此行为已得到修复。

4.本文总结

尽管在 Node.js 流和加密模块中有一些回归,但与以前的版本相比,Node.js 20 在性能上有了显著改进。 在属性访问、URL 解析、缓冲区/文本编码和解码、启动/进程生命周期时间和 EventTarget 等 JavaScript 操作中观察到了显著的增强。

Node.js 性能团队 (nodejs/performance) 扩大了其范围,从而在每个新版本的性能优化方面做出更大贡献。 这种趋势表明 Node.js 将随着时间的推移继续变得更快。

值得一提的是,基准测试侧重于特定操作,这可能会或可能不会直接影响特定用例。 因此,我强烈建议查看
state-of-nodejs-performance 存储库中的所有基准测试结果,并确保这些操作符合您的业务需求。


参考资料

原文链接:
https://blog.rafaelgss.dev/state-of-nodejs-performance-2023