技術前沿:前端必知 2023 年 Node.js 綜合性能

高級前端進階 發佈 2023-05-28T18:37:08.789925+00:00

大家好,很高興又見面了,我是"高級前端‬進階‬",由我帶著大家一起關注前端前沿、深入前端底層技術,大家一起進步,也歡迎大家關注、點讚、收藏、轉發!高級前端‬進階今天給大家帶來的是 blog.rafaelgss.dev 平台發布的一篇《State of Node.

家好,很高興又見面了,我是"高級前端‬進階‬",由我帶著大家一起關注前端前沿、深入前端底層技術,大家一起進步,也歡迎大家關注、點讚、收藏、轉發!

今天給大家帶來的是 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 腳本

//1
git clone git@github.com:RafaelGSS/node.git
//2
cd node && git checkout state-of-nodejs
//3
nvm install v20.0.0
cp $(which node) ./node20
nvm install v18.16.0
cp $(which node) ./node18
nvm install v16.20.0
cp $(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

關鍵字: