Misskey & Webテクノロジー最前線

BunはNodeより速いのか? Misskeyで検証

本連載では分散型マイクロブログ用ソフトウェアMisskeyの開発に関する紹介と、関連するWeb技術について解説を行っています。

今回はNode.js互換のJavaScriptランタイム、Bunのパフォーマンスについて、Misskeyのコードベースを用いて検証を行います。

Bunとは

Bunは、Node.js(以下Node)互換である後発のJavaScriptランタイムです。

JavaScriptエンジンにNodeで採用されているV8ではなくJavaScriptCoreを採用しているほか、TypeScriptを事前コンパイルなしに実行することもできます。

肉まんのようなマスコットキャラクターが特徴です。

モチベーション

そんなBunの公式サイトではNodeよりも大幅に性能上のアドバンテージがあるように紹介されていますが、こうした競合ソフトウェアとの一方的な比較は得てして限られた条件での有利な部分だけを強調したものになりがちです。

たしかに一部の処理速度は競合と比べて速かったとしても、様々なタイプの処理が存在する実際の環境では、全体的にみるとむしろ遅くなってしまうということはよくあり[1]、筆者はこの手のベンチマークには懐疑的な見方をすることが多いです(筆者がいわゆる逆張り的な性格であるというのも否定はしませんが⁠⁠。

そこでMisskeyという比較的複雑な実アプリケーションで検証すれば、より公平なパフォーマンスの比較が行えると思い今回の検証に至りました。

もちろんBunのほうが優れていればMisskeyでも採用したいと考えているので、そのための調査という目的もあります。

ノート一般的に、成熟した既存のソフトウェア(今回で言えばNode)は、様々なエッジケースやセキュリティ対応が多く含まれるため、若干パフォーマンス面では劣る場合もあります。そのため一概に速ければ正義というわけではなく、こう言うと語弊があるかもしれませんが「速いソフトウェアは代わりに安全性(脆弱性の少なさ⁠⁠・安定性(バグの少なさ)が犠牲になっている」こともあるので、選定する際は速さだけではなく、堅牢性等も考慮に入れることが望ましいでしょう。

また、宣伝は鵜呑みにせず多少眉に唾をつけて捉え、実際の環境で検証を行ってみることが大切です。

ちなみにNodeより後発のJavaScriptランタイムとしては他にDenoがありますが、DenoはBunほどには性能面をウリにはしていない印象があります。

おことわり

今回の検証は、高精度なNodeとBunのベンチマークを提供することは意図していません。あくまでも限られた環境・ユースケースでの1サンプルとして考えてください。

また、⁠全く同じ既存コードをいかに速く実行できるか」という観点で検証していますので、Bun専用のコードに書き直すといった改修(チューニング)は行なっていません。

Nodeと同じAPIに対応する以上、Nodeと同じくらい、あるいはNodeより速く処理できるのか厳しく見ていきます。本質的には「ある仕様の機能をいかに効率よく実装できるか」ということなので、どちらが条件的に有利/不利というのはありません。

準備

基本的にBunはNode互換とはいえまだ完全ではないので、まずMisskeyをBunでも動くように改修する必要がありました。

互換性に関するステータスは次のドキュメントにまとめられています。

Misskeyで使用しているcluster APIを始め、いくつかのAPIがまだBunに実装されていなかったので、その部分は検証にあたって削除しました。また、Misskeyが依存しているパッケージも動かないものがちょこちょこありましたので、使わないように変更しました。

移行作業中に困ったこととしては、互換性がない機能を使った場合に無言でプロセス終了するケースがあることです。エラーメッセージが出る場合は原因を特定できますが、出ない場合だと原因の特定が難しかったです。

検証環境

今回の検証環境は次のとおりです[2]

  • OS: Ubuntu 22.04.4
  • CPU: i9-13900K
  • Node: 22.6.0
  • Bun: 1.1.22
  • Misskey: 2024.7.0

関数処理時間の測定は、consoleのtime/timeEndを使用して行いました。

結果

前置きが長くなりましたが、以下が検証結果です。

起動直後のメモリ消費量

Misskey起動直後のメモリ消費量です。

  • Node: 約200MB
  • Bun: 約800MB

Bunのほうがメモリ消費量は大幅に多くなっています。

起動に要する時間

Misskeyをコマンドラインから起動して、HTTPサーバーとしてportをlistenするまでに要する時間です。

  • Node: 約5秒
  • Bun: 約5秒

結果はほぼ互角でした。CPUをアンダークロックすれば違いが出てくるかもしれませんが今回は検証できませんでした。ただ少なくともどちらかが劇的に速い、ということはなさそうです。

トップページのHTTPリクエスト

トップページのHTTPリクエストは、基本的なHTTPサーバー性能やpugのレンダリング性能が試されます。

今回はベンチマークツールとして、autocannonを使用しました。次のコマンドを実行します。

$ autocannon http://127.0.0.1:3000/

NodoとBunそれぞれの結果は次のようになりました。

Node
┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐
│ Stat    │ 2.5%   │ 50%    │ 97.5%  │ 99%    │ Avg       │ Stdev    │ Max    │
├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤
│ Latency │ 100 ms │ 116 ms │ 161 ms │ 201 ms │ 120.81 ms │ 18.29 ms │ 243 ms │
└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘
┌───────────┬────────┬────────┬─────────┬─────────┬─────────┬────────┬────────┐
│ Stat      │ 1%     │ 2.5%   │ 50%     │ 97.5%   │ Avg     │ Stdev  │ Min    │
├───────────┼────────┼────────┼─────────┼─────────┼─────────┼────────┼────────┤
│ Req/Sec   │ 67     │ 67     │ 84      │ 91      │ 82.3    │ 8.22   │ 67     │
├───────────┼────────┼────────┼─────────┼─────────┼─────────┼────────┼────────┤
│ Bytes/Sec │ 856 kB │ 856 kB │ 1.07 MB │ 1.16 MB │ 1.05 MB │ 105 kB │ 855 kB │
└───────────┴────────┴────────┴─────────┴─────────┴─────────┴────────┴────────┘

Req/Bytes counts sampled once per second.
# of samples: 10

833 requests in 10.05s, 10.5 MB read
Bun
┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐
│ Stat    │ 2.5%   │ 50%    │ 97.5%  │ 99%    │ Avg       │ Stdev    │ Max    │
├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤
│ Latency │ 105 ms │ 146 ms │ 193 ms │ 202 ms │ 147.14 ms │ 23.36 ms │ 238 ms │
└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘
┌───────────┬────────┬────────┬────────┬─────────┬────────┬─────────┬────────┐
│ Stat      │ 1%     │ 2.5%   │ 50%    │ 97.5%   │ Avg    │ Stdev   │ Min    │
├───────────┼────────┼────────┼────────┼─────────┼────────┼─────────┼────────┤
│ Req/Sec   │ 56     │ 56     │ 68     │ 80      │ 67.3   │ 6.98    │ 56     │
├───────────┼────────┼────────┼────────┼─────────┼────────┼─────────┼────────┤
│ Bytes/Sec │ 712 kB │ 712 kB │ 865 kB │ 1.02 MB │ 856 kB │ 88.7 kB │ 712 kB │
└───────────┴────────┴────────┴────────┴─────────┴────────┴─────────┴────────┘

Req/Bytes counts sampled once per second.
# of samples: 10

683 requests in 10.07s, 8.56 MB read

Latencyは小さいほどベターで、Req/Secは大きいほどベターですが、若干Nodeのほうがパフォーマンスが良い結果になりました。

投稿詳細ページのHTTPリクエスト

投稿詳細ページのHTTPリクエストは、トップページと異なり、投稿の情報をHTMLに埋め込むためより負荷が高くなります。

autocannonを使ったベンチマーク結果は次のようになりました。

Node
┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐
│ Stat    │ 2.5%   │ 50%    │ 97.5%  │ 99%    │ Avg       │ Stdev    │ Max    │
├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤
│ Latency │ 258 ms │ 270 ms │ 386 ms │ 473 ms │ 276.32 ms │ 33.38 ms │ 484 ms │
└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘
┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐
│ Stat      │ 1%     │ 2.5%   │ 50%    │ 97.5%  │ Avg    │ Stdev   │ Min    │
├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤
│ Req/Sec   │ 22     │ 22     │ 38     │ 39     │ 35.9   │ 4.89    │ 22     │
├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤
│ Bytes/Sec │ 296 kB │ 296 kB │ 511 kB │ 524 kB │ 483 kB │ 65.7 kB │ 296 kB │
└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘

Req/Bytes counts sampled once per second.
# of samples: 10

369 requests in 10.07s, 4.83 MB read
Bun
┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐
│ Stat    │ 2.5%   │ 50%    │ 97.5%  │ 99%    │ Avg       │ Stdev    │ Max    │
├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤
│ Latency │ 263 ms │ 298 ms │ 486 ms │ 488 ms │ 308.54 ms │ 42.78 ms │ 488 ms │
└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘
┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐
│ Stat      │ 1%     │ 2.5%   │ 50%    │ 97.5%  │ Avg    │ Stdev   │ Min    │
├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤
│ Req/Sec   │ 20     │ 20     │ 31     │ 39     │ 32.4   │ 5.11    │ 20     │
├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤
│ Bytes/Sec │ 268 kB │ 268 kB │ 415 kB │ 522 kB │ 434 kB │ 68.4 kB │ 268 kB │
└───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘

Req/Bytes counts sampled once per second.
# of samples: 10

334 requests in 10.06s, 4.34 MB read

投稿詳細ページでも若干Nodeのほうがパフォーマンスが良い結果になりました。

タイムライン取得

Misskeyでもっともよく実行される処理のひとつであろう、ホームタイムラインの取得処理です。複雑なSQLの組み立て処理などが含まれます。

notes/timeline APIのgetFromDbforで1,000回連続して行った場合の処理時間は

  • Node: 平均約14秒
  • Bun: 平均約19秒

となり、Nodeのほうが若干速いという結果になりました。

投稿の作成

投稿の作成はそれをトリガーとしていろいろな処理が行われるため、比較的重めです。

NoteCreateServicecreateforで1,000回連続して行った場合の処理時間は

  • Node: 平均約5秒
  • Bun: 平均約10秒

となり、Nodeのほうが2倍程度速いという結果になりました。

identiconの生成

identiconは、ユーザーごとに異なる初期アイコンを提供する機能で、サーバーサイドの画像処理になります。genIdenticonをランダムなシードでforで10,000回連続して行った場合の処理時間は

  • Node: 平均約6.9秒
  • Bun: 平均約6.9秒

となり、結果は互角でした。

signedPost

signedPostはActivityPubのアクティビティを生成して署名する処理で、Misskeyの中では比較的重めの処理です。

ApRequestServicesignedPostを`forで100回連続して行った場合の処理時間は

  • Node: 平均約1秒
  • Bun: 平均約35秒

となり、BunがNodeより大幅に遅い結果が得られました。ただ、これに関しては既知の問題のようです(本記事執筆時点では修正されていません⁠⁠。

AiScriptの実行

Misskeyで使用しているAiScriptインタプリタの実行時間です。AiScriptはブラウザでも動くため、NodeのAPIは使っておらず、⁠特定のランタイム向けのチューニング」の余地がない純粋なJavaScript実装になっています。

次のAiScriptコードを計測しました。検証にあたっては高負荷を防ぐためのスリープ機能はオフにしています。

for (let i = 1, 100000) {
	let msg =
		if (i % 15 == 0) "FizzBuzz"
		elif (i % 3 == 0) "Fizz"
		elif (i % 5 == 0) "Buzz"
		else i
}

結果は

  • Node: 平均約3.5秒
  • Bun: 平均約5.0秒

となり、Nodeのほうがかなり速い結果に。V8の底力でしょうか。

WebSocketのメッセージ送信

ランダムなメッセージ内容で、sendMessageToWsforで100,000回連続して行った場合の処理時間を計測した結果、次のようになりました。

  • Node: 平均約0.3秒
  • Bun: 平均約0.1秒

また、ランダムなメッセージ内容で、sendMessageToWsforで100,000回連続して行い、そのすべてがクライアント側に到達するまでの時間を計測した結果、次のようになりました。

  • Node: 平均約1.2秒
  • Bun: 平均約1.3秒

WebSocketに関しては評価が難しい結果になりました。sendMessageToWs関数の実行に要する時間」で見るとBunのほうが優れていますが、⁠メッセージが到達するまでの時間」で見るとわずかにNodeのほうが優れていました。

これはクライアント側の受信処理がネックになって本来の差が隠されてしまっているのか、もしくはBunがメッセージを内部的にバッファリングして、関数自体は早めに切り上げているため見かけ上速く計測されている、等の理由なのかはわかりません。

ただしBunの場合バグか仕様かはわかりませんが、一度に多量の送出が重なると捨てられてしまうメッセージがあることがわかったため、WebSocketに関してこれ以上この方法でのパフォーマンス比較は叶いませんでした。

とはいえ今回行った検証の中では、唯一Bunのポテンシャルが感じられた結果になりました。

その他

全投稿のエクスポート機能など他にも検証したい処理がありましたが、コードがあまりにもBunと互換性がなく、改修に時間を要する雰囲気だったので今回は断念しました。機会があればリベンジしたいと思います。

まとめ

今回の検証では、全体的にNodeのほうが速い結果になり、Bun側でアピールされているような10倍近いNodeとの性能差は確認できませんでした。メモリに関しても、Nodeのほうが省メモリでした。

Bun向けにチューニングを行えば結果が変わるシチュエーションもあると思いますが、いずれにせよ現状は手放しに「既にNodeで動いているコードをBunで動かしただけで速くなる」というわけではなさそうです。

また、AiScriptの実行など、チューニングの余地がない処理でもBunが遅い結果が出ていますので、根本的な「JavaScriptCoreとV8の差」もあるのかなと感じました。

とはいえ今後Bun側の最適化によってチューニングなしに高速化することは十分考えられますし、今回検証しなかった部分ではBunのほうが速い場合もあると思います。逆にNode側のさらなる高性能化も今後あり得るので、検証はこれからも続けていきたいです。

注意繰り返しになりますがあくまで今回の検証は1サンプルにすぎないことは留意してください。同一の検証方法でも他の環境では違った結果になる可能性もあります。

そして、今回は性能面でのみNodeとBunを比較しました。当然ですが性能以外の面、例えば機能性やドキュメントの充実度等はまた異なるため、性能が劣っているほうは選択する価値がないのかというとそれはNoです。

Bunの発展に期待するとともに、Misskeyでも、今後どちらをメインに採用するのか慎重に検討していきたいと思います。

おすすめ記事

記事・ニュース一覧