TechFeed Experts Night Pick up

Bunファーストインプレッション - JavaScriptランタイム界に赤壁の戦い”! ~TechFeed Experts Night#8講演より

本記事は、2022年11月に開催された「TechFeed Experts Night#8 ~ JavaScriptランタイム戦争最前線」のセッション書き起こし記事「Bunファーストインプレッション - JavaScriptランタイム界に⁠赤壁の戦い⁠を!」を転載したものです。オリジナルはTechFeedをご覧ください。

Japan Node.js Associationの理事をしています、古川と申します。ソーシャルのIDはこちらになります。

JSConf JPを2022年11月26日に開催予定で、今回のテーマであるNode.JS / Deno /Bunに関連するセッションもありますので、ご興味のある方はぜひご参加よろしくお願いします(注: このイベントは終了しました⁠⁠。

JSConf JP

今日はBunについてお話しします。過去にNode学園でお話しした内容とほとんど同じですが、聞いたことがある方はおさらいとして聞いていただければと思います。

Bunとは

Bunは新しいJavaScriptのランタイムで、Node.jsDenoと同じポジションです。特徴的なのは「JSCが使われている」⁠Next.jsが動く」⁠TypeScript Transpilerが同梱されている」で、後ほどそれぞれについて解説します。

Bunとは

ちなみに2022年1月ごろには私もBunのことをキャッチしていて、社内でもZigで書かれていることなどに注目していました。当時のBunのWebサイトのOGタグには「JavaScript runtime environment」とも記載があるものの、最初に「Bun is a new JSX/TypeScript transpiler」と書かれていて、TranspilerやBundlerとしての印象が強く、RomeのようにRustで書かれた開発ツール群に類するものと考えていました。

その後、2022年8月ごろにBun v0.1がリリースされ、Next.jsが動くようになったり、Webサイトが刷新されたりして一気に有名になりました。

2022年1月ごろにはBunのことをキャッチしていた

ちなみにBunという名前は、BundleのBunと、それらを包み込むという意味から中華まん(Bun)に由来しています。ただし、これはYouTubeでの対談の中で語られただけで、公式には書かれておらず、もしかしたらそのうちかっこいい別の由来がつけられるかもしれません。

なぜBunというのか?

Bunが目指すところは、主に3つあります。

  • 高速に起動すること
  • 次世代のパフォーマンス
  • ツールとしてgreat and completeであること (Bundler / Transpiler / Package managerを内包すること)
Bunが目指すところ

また、BunのWebサイトの最初にはこう書かれています。

Bun is designed as a drop-in replacement for your current JavaScript & TypeScript apps or scripts — on your local computer, server or on the edge. Bun natively implements hundreds of Node.js and Web APIs, including ~90% of Node-API functions (native modules), fs, path, Buffer and more.

The goal of Bun is to run most of the world's JavaScript outside of browsers, bringing performance and complexity enhancements to your future infrastructure, as well as developer productivity through better, simpler tooling.

  • Bunは既存のJavaScript / TypeScriptアプリケーションやスクリプト(ローカルコンピュータで動いているもの、サーバで動いているもの、Edgeで動いているものすべて)のランタイムを完全に置き換えられるように設計されている
  • 数百のNode.js API / Web APIをネイティブで実装することを目指している(Node.jsのAPIと互換性があり、ローカルコンピュータやサーバで動いているNode.jsを置き換えることができ、Edgeでも動かせる)
  • Bunのゴールは、ブラウザ外で動くJavaScriptのランタイムとしてのシェアNo.1である。さらにパフォーマンスや複雑さの改善を将来のインフラストラクチャに取り込み、より良いシンプルなツールセットにより開発生産性を実現する

このように、Bunは手元のPCであろうとEdgeであろうと、既存のJavaScript / TypeScriptランタイムをすべて置き換えることを目標とした、とても意欲的なプロジェクトです。そのためにWeb APIとNode.js APIをある程度動くところまでサポートしています。Next.jsが動くのはこのWeb API / Node.js APIサポートのおかげで、この辺りが昔のDenoなどの他のランタイムとは若干違うマーケティング戦略といえるでしょう。

現在Bunがサポートしている機能

Web APIのうち、Fetch / WebSocket / ReadableStreamなどはすでにBunに実装済みです。

Fetch ,WebSocket ,ReadableStreamなどは実装済み

node_modulesのモジュール解決アルゴリズムも最初から実装されていて、Node.jsのモジュール解決も動き、もちろんESMも動きます(むしろESMのほうが第一市民感があり、Bunの内部ではESMが使われています⁠⁠。

node_modulesのモジュール解決アルゴリズムも最初から実装

BunではTypeScript/JSXで書かれたコードもそのままトランスパイルされ、またBun.TranspilerというAPIも用意されているので、BunのJSXやTypeScriptトランスパイラを呼び出してカスタマイズすることもできます。

TypeScript/JSXで書かれたコードもそのままトランスパイル

Bunにはsqliteが最初から同梱されていて、import sqlite from ⁠bun:sqlite⁠;で使えます。Edgeではsqliteを使うことも多いので、そのために同梱されているのかもしれません。.envファイルで環境変数をロードする処理も最初から対応しており、DX(開発体験)も考慮されています。

sqliteが最初から同梱

驚いたのはNode-API(N-API: C++などで書かれたネイティブモジュールをラップするための関数)を実装済みなので、Node.jsのネイティブモジュールもだいたい動くということです。さらに、bun:ffi(Foreign Functional Instructions: 外部関数呼び出しができるインターフェース)もあり、FFI呼び出しでもネイティブコードを動かせます。これらの機能により、C++やC言語などで書かれたネイティブモジュール(とくにラッパーを使ってNode.jsから提供されているもの)もBunでは使えますし、Node.jsで提供されているものでなくても、FFIで使えるものであればBunでは使えます。

このあたりのネイティブモジュールへの対応は、昔PHPでFacebookがHHVM(PHPの仮想マシン)を作った際に、ネイティブモジュール呼び出しをサポートすることで移行コストを削減した戦略に似ていると感じます。

Node-APIを実装済み

Bunのパフォーマンス

BunのWebサイトにはv0.2の最新のパフォーマンスグラフが「Bunがかなり速い」という触れ込みで掲載されていて、デモページでも高速な動作を確認できますが、このベンチマーク結果だけでは、かならずしもBunが実用的に高速であるとはいえなさそうです。

Bunが速いという触れ込み

このベンチマークで何をしているのかを見てみると、少なくともv0.1の段階ではすごく単純なことしかしていません。

こちらはBunのトップページに掲載されている「Server-side rendering React」のパフォーマンス測定に使用されたベンチマークスクリプトですが、Hello WorldのHTMLを生成しているだけです。Reactのサーバサイドレンダリングといえば、通常はコンポーネントが絡み合った複雑なページであることが多いので、この結果だけで実用のパフォーマンスについて判断するのは難しいです。とくに各コンポーネントが1つずつファイルに分割されていて、それらをインポートしたり、その他のファイルを読み込んだりするところは本来はもっとも時間のかかる部分です。Bunのページで紹介されているベンチマークはとくにこのような処理もなく、ただ文字列を連結して出力しているだけですので、renderToReadableStreamの部分でパフォーマンスに差が出ている可能性があります。

ベンチマークスクリプトはHello Worldを生成しているだけ

Bunのページで紹介されているベンチマーク結果を見ると、他のランタイムに比べて3倍以上の開きがあり、Bunが非常に高速に見えますが、実際はマイクロベンチマーク的な内容であり、特定の関数をピックアップした結果と考えたほうがいいでしょう。ただ、BunではrenderToStreamもZigで実装しているので、この特定の関数についてはJavaScriptで実装している他のランタイムと比較して速いというのは正しいと思います。

Bunが速い理由 - Node.jsとの比較

Bunのサイトでは、V8ではなくJSCを採用したこと(JSCのほうがメモリ効率が良く、JITのやり方が違うため)と内部のコードにZigを採用したのがミソと書かれていますが、私はBunと他のランタイムのパーフォマンス差には別の理由があると思っています。

なぜBunが速いのか

Bunのパフォーマンスを理解するために、Node.jsとBunの内部を細かく比較してみましょう。

Node.jsの内部構造はおおよそこのようになっています。

Node.jsの内部構造

同じようにBunの内部構造を表現すると、このようになります。

Bunの内部構造
  • Node.jsではV8だったところが、BunではJavaScriptCoreになっている
  • http-parser → pico-http-parser、libuv → kqueue/iouring
  • http/2, quick層は未実装(v0.1時点)

Standard Library - APIの互換性

Node.jsとBunはいちおう互換性があると言われていますが、今現在もchild_processやdnsなどNode.js互換のAPIを作っているところなので、BunがNode.jsとの互換性を保とうとしている(これから実現する)と考えるのが正しいでしょう。ただし、私が試したときにはBufferがまったく動かなかったりしましたので、APIの互換性については発展途上にあるようです。

今Node.js互換APIを作っているところ
Bufferがまったく動かなかった

Node.js互換のAPIを絶賛追加中なので、そのうち完全互換になるかもしれませんが、一方で、Node.js側のAPIに変更があるたびにBunが追従するのは難しいと考えています。ユースケースの8割ほどをカバーしてよしとするか、WINTER CG(JavaScriptランタイムを作る際にブラウザとの親和性を高めることを目的とした仕様化団体)ベースのWeb APIをサポートした時点でNode.jsのAPIとの100%互換性は諦めるのではないか、と私は想像しています。

そのうち完全互換になるかも

HTTP Parserの違い - http-parser vs pico-http-parser

Node.jsのhttp-parserはかなり変わっていて、TypeScriptで書くとC言語で出てくるという特殊なものです。一方でpico-http-parserはkazuhoさん(奥一穂氏)が作られた、C言語業界では誰もが使ったことがある高速なパーサです。

http-parser vs pico-http-parser

http-parserとpico-http-parserは趣きが違います。http-parserはヘッダをパースした時点でNode.jsにプロセスが移るようになっていて、Node.jsにとって使いやすいように作られています。pico-http-parserでは全体をまとめてパースするため、どれだけ短時間でパースできるかということに重点が置かれています。

JavaScriptエンジンの違い - V8 vs JSCの最適化

最近のJavaScriptエンジンは単なるインタプリタではなく、Just In Timeコンパイラにより統計的に状況をみて高速化されます(バイトコードから機械語に翻訳する時点で最適化されます⁠⁠。このJITによる最適化のプロセスがV8とJSCでは異なります。

JavaScriptエンジンの違い
最近のJavaScriptエンジン
JITによる最適化のプロセス
V8 vs JSC

クルマに例えるとギア(変速機)の数が違い、V8は3段変速ギア、JSCは4段変速ギアと捉えることができるでしょう。クルマのギアであれば、低速ギアは加速は速いものの最高速は遅く、高速ギアは加速が遅いものの最高速は速いので、走行条件に応じてギアを切り替える必要があります。 V8やJSCのJITでも同じことが行われていて、コードにあわせてギアを切り替えながら最適化しています。

変速ギアの数が違う

V8は3段変速ギアと表現できます。

  • Ignition(1速)… 単にbytecodeを生成する
  • Sparkplug(2速)… bytecodeから変数解決/脱糖衣構文化などを実施する
  • Turbofan(3速)… 統計情報をもとに型レベルでの最適化を実施する

一方でJSCは4段変速ギアと表現できます。

  • LLInt(1速)… 単にbytecodeを生成する
  • Baseline JIT(2速 … bytecodeから簡単なJIT生成コードを作る
  • DFG JIT(3速)… データフローに基づく最適化を実施し、主に戻り値の型チェックなどを行って最適化する
  • FTL JIT(4速)… SSAによる最適化(型レベルでの最適化)を実施する(V8のTurbofanと同様)

IO抽象化レイヤの違い - libuv vs kqueue/iouring

Node.jsのlibuvはWindowsもサポートするために下位のOSレベルのAPIの違いを吸収するために複雑/巨大化しています。ただし現代では、WSLによりWindows上でLinuxシステムが動作しますので、WindowsもLinuxと同様に扱えます。そのため、BunではmacOSのみkqueue、Linuxはio_euringを使い分けています。これは現代の環境に即した正しい構成だと思います。

libuv vs kqueue/iouring

まとめ - JavaScriptランタイムは”戦国時代”に突入!?

ここまで見てきたように、BunはNode.jsを置き換えるためにNode.js側の実装に歩み寄っていて、DenoがこれからやろうとしているNode.jsとの互換性確保を先立って実装してきた印象があります。これはマーケティング戦略としておもしろいものだと思います。

Bunの戦略

ただ、Bunがプロダクションレディであるかというと、まだまだという印象です。とはいえBunは現在v0.2ですので、これからBunを盛り上げていきたいという方は使ってみるといいと思います。また、コントリビューションのチャンスは多そうですので、Bunが盛り上がりそうと感じる方はプロジェクトに貢献してNext JavaScript Runtimeのデファクトを共に狙っていくのもありだと思います。

Bunのこれから

個人的にはJavaScriptランタイムはNode、Deno、Bun、Cloudflare Workersなどを交えて今戦国時代が始まったと感じています。三国志にたとえると、魏=Node.js / 呉=Deno / 蜀=Bunのように見えます。これらのランタイムはどれもNode.js互換を保つことを目指しているようですが、そのために機能差がなくなってきてしまっていて、あまりおもしろくないところもあります。機能的にはどれも同じでパフォーマンスだけで勝負するよりも、私はむしろ圧倒的に違うポイントを見せて差別化するという赤壁の戦いを見せてほしい、そのほうがおもしろくなると思っています。

JavaScriptランタイムのこれから

一方でJavaScriptランタイムの互換性については、WINTER CGと呼ばれるWeb互換のJavaScriptランタイム仕様を決めようとする動きも出ているため、この流れは追っていきたいところです(こちらについてはJSConf JPでもお話する予定です⁠⁠。

WINTER CG
WINTER CGの流れは追っていきたい

以上で発表を終了します。ありがとうございました。

おすすめ記事

記事・ニュース一覧