DMM.comの、一番深くておもしろいトコロ。

Node.jsから爆速の肉まん(Bun.sh)に置き換えて速度を比較してみる

Node.jsから爆速の肉まん(Bun.sh)に置き換えて速度を比較してみる

  • このエントリーをはてなブックマークに追加

Node.jsから爆速の肉まん(Bun.sh)に置き換えて速度を比較してみる

この記事は、DMMグループAdvent Calendar 2022 9日目の記事になります。

こんにちは。技術支援チームの四戸義龍と申します。
普段は負債脱却支援などのための社内全般で使うツールの開発・保守や、社内用の認証・認可基盤に関する業務を行っています。

今回は今年の夏頃に話題になっていたBun.shを、実際のプロダクトに置き換えて見た際にどれだけ高速になるのか検証した結果を記事にしました。

そもそもBun.shとは?

Node.jsやDenoなどと同じ様なJavascriptランタイムです。
github.com

Node.jsやDenoではV8というJavascriptエンジンを使用しているが、Bun.shではSafariなどで利用されているJavaScriptCoreを採用しており、開発はZigという言語を使用しています。
Zigはオープンソースのプログラミング言語であり、C言語よりも型安全性などが高く、またデフォルトではメモリ管理できずオプションでヒープ利用など、Rustなどに近い印象がありますが、Rustほど複雑ではありません。
また、Wasmを小さくできる点と、 ビルドの標準のオプションでWasmが吐ける点がおそらく採用された理由ではと思っています。 ziglang.org

また、Bun.shはnpmもサポートしており、環境移行性も高いです。
(Bun.shに後押しをされたのか、Denoもnpmサポートをする様になりました)
TypeScriptファイルも自動でトランスパイルしてくれるため、TypeScriptを使う場合にも便利です。

ただし、課題もあります。
現状はNode APIには一部しか対応しておらず、Denoほどの互換性もありません。
まだ出たてなので、不具合も多くbug報告もかなりあります。
また、Bun.shは700万ドル程の資金調達は完了したものの、現在人材募集中で運営もまだ不安定です。
プロダクトに反映する場合は細心の注意を払って利用する様にしてください。

環境について

環境については下記のとおりです。

項目 version
機器 MacBook Pro (13-inch, 2019, Two Thunderbolt 3 ports)
プロセッサ 1.4 GHz クアッドコアIntel Core i5
メモリ 16 GB 2133 MHz LPDDR3
Node 18.10.0
Bun.sh 0.2.2

実行するプロダクトは結構古いプロダクトなのでバージョンなど使っている技術も古いです。

項目 version
Typescript 3.9.10
Vue 2.6.10

モジュールのインストール速度

公式サイトとBun.shのGithubを確認するとインストールも最適化されているらしく非常に早いらしいので、こちらから検証していきます。

Replace yarn with bun install and get 20x faster package installs.

公式が言うには20倍早いらしい。

npm install

約 50.8sec

time npm install               

/** 中略 **/ 

added 1692 packages, and audited 1693 packages in 50s

130 vulnerabilities (14 low, 37 moderate, 53 high, 26 critical)

To address issues that do not require attention, run:
  npm audit fix

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.
npm notice 
npm notice New major version of npm available! 8.19.2 -> 9.1.3
npm notice Changelog: https://github.com/npm/cli/releases/tag/v9.1.3
npm notice Run npm install -g npm@9.1.3 to update!
npm notice 
npm install  32.04s user 16.24s system 95% cpu 50.818 total

Bun.sh

約 8.7秒

time bun install 
bun install v0.2.2
 + @types/jest@23.3.14
 + @types/uuid@3.4.4
 + @vue/cli-plugin-babel@3.12.1
 + @vue/cli-plugin-e2e-nightwatch@3.12.1
 + @vue/cli-plugin-eslint@3.12.1
 + @vue/cli-plugin-typescript@3.12.1
 + @vue/cli-plugin-unit-jest@3.12.1
 + @vue/cli-service@3.12.1
 + @vue/eslint-config-standard@4.0.0
 + @vue/eslint-config-typescript@4.0.0
 + @vue/test-utils@1.0.0-beta.29
 + babel-core@7.0.0-bridge.0
 + babel-eslint@10.1.0
 + eslint@5.16.0
 + eslint-plugin-vue@5.2.3
 + ts-jest@23.10.5
 + typescript@3.9.10
 + vue-template-compiler@2.7.14
 + axios@0.18.1
 + bootstrap-vue@2.0.0-rc.28
 + core-js@2.6.12
 + inversify@5.0.5
 + reflect-metadata@0.1.13
 + uuid@3.3.3
 + vue@2.7.14
 + vue-class-component@7.2.6
 + vue-multiselect@2.1.6
 + vue-property-decorator@8.5.1
 + vue-router@3.6.5
 + vuex@3.1.1
 + vuex-typex@3.1.5

 1544 packages installed [8.63s]
bun install  1.53s user 10.49s system 137% cpu 8.721 total

約5.8倍の結果になりました。
非常に高速な結果になりましたが、これはnpmのバージョンが古いため速度差が非常に大きくなっている可能性もあります。
流石に20倍ほど早くはありませんでしたが結構早いです。

起動速度

開発環境の立ち上げ速度を計測します。
vue-cli-service serve をpackage.jsonに記載しています。
bunの実行は bun run xxxxで実行可能です。
bunはnpm互換性があって使いやすいです。

Replace npm run with bun run and save 160ms on every run.
Bun runs package.json scripts 30x faster than npm run bun.sh

公式が言うには30倍早いらしいです。

npm

約4.8sec

npm run serve

> dsearch@0.1.0 serve
> vue-cli-service serve

 INFO  Starting development server...
Starting type checking service...
Using 1 worker with 2048MB memory limit
13% building 30/33 modules 3 active /Users/shinohe-yoshi/d-search/client/dsearch/node_modules/querystring-es3/decode.js=============

/** 中略 **/

98% after emitting CopyPlugin

Version: typescript 3.9.10
Time: 4768ms

  App running at:
  - Local:   http://localhost:1024/ 
  - Network: http://192.168.111.100:1024/

  Note that the development build is not optimized.
  To create a production build, run npm run build.

Bun.sh

約3.4sec

  bun run serve
  $ vue-cli-service serve
   INFO  Starting development server...
  Starting type checking service...
  Using 1 worker with 2048MB memory limit
   40% building 208/220 modules 12 active ...modules/bootstrap-vue/es/utils/array.jsBrowserslist: caniuse-lite is outdated. Please run next command `npm update`
   98% after emitting CopyPlugin                                                    

/** 中略 **/

  Version: typescript 3.4.5
  Time: 3443ms

    App running at:
    - Local:   http://localhost:1024/ 
    - Network: http://192.168.111.100:1024/

    Note that the development build is not optimized.
    To create a production build, run npm run build.

1秒ばかり早かったですが、30倍の結果が出て欲しいところでしたが、誤差と言っても良いレベルですね。 もう少しmoduleが大量にある場合は差が結構出るかもしれません。

ビルド速度

npm run build

$time npm run build

> dsearch@0.1.0 build
> vue-cli-service build


⠙  Building for production...Starting type checking service...
Using 1 worker with 2048MB memory limit
⠹  Building for production...

/** 中略 **/

npm run build  10.00s user 0.95s system 155% cpu 7.024 total

Bun.sh run build

$ time bun run build
$ vue-cli-service build

⠙  Building for production...Starting type checking service...
Using 1 worker with 2048MB memory limit
⠹  Building for production...

/** 中略 **/

bun run build  9.62s user 0.85s system 156% cpu 6.679 total

実行結果を見る限り内部で叩かれているコマンドは同じそうなので、やっぱりほとんど変化ないですね。

サーバーサイドレンダリング速度

これは負荷ツールを利用して計測します。
負荷ツールはJMeterとか色々あるんですが、個人的に最近良さげと思っているVegetaで検証します。
余談ですが、Githubの公式のド●ゴンボールの●ジータのイラストが堂々と貼ってあります。
そのうち鳥山先生に怒られろ。

github.com

Vegeta Version 情報

vegeta -version
Version: 12.8.4

npm vue-cli-service serve

$ echo 'GET http://localhost:1024' | vegeta attack -duration=5s | vegeta report
Requests      [total, rate, throughput]         250, 50.20, 50.17
Duration      [total, attack, wait]             4.983s, 4.98s, 2.567ms
Latencies     [min, mean, 50, 90, 95, 99, max]  840.133µs, 1.785ms, 1.602ms, 2.267ms, 2.832ms, 6.55ms, 18.412ms
Bytes In      [total, mean]                     234500, 938.00
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:250  
Error Set:

Bun.sh vue-cli-service serve

$ echo 'GET http://localhost:3000' | vegeta attack -duration=5s | vegeta report
Requests      [total, rate, throughput]         250, 50.19, 50.17
Duration      [total, attack, wait]             4.983s, 4.981s, 1.674ms
Latencies     [min, mean, 50, 90, 95, 99, max]  651.314µs, 1.798ms, 1.76ms, 2.015ms, 2.291ms, 2.556ms, 13.7ms
Bytes In      [total, mean]                     180500, 722.00
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:250  
Error Set:

結果だけ見るとほとんど変わらない結果になりました。 実行結果を見る限り内部で叩かれているコマンドは同じそうなので(vue-cli-service serve)、 Github内の下記の記述どおり、おそらくbunプロセスでは動いてない可能性があります。

bun run ${script-name} runs the equivalent of npm run script-name. For example, bun run dev runs the dev script in package.json, which may sometimes spin up non-bun processes.

そのため、それぞれサーバーを起動し確認してみます。

Node + express

$ echo 'GET http://localhost:1024' | vegeta attack -output=./node_report.bin -duration=5s | vegeta report ./node_report.bin
Requests      [total, rate, throughput]         250, 50.19, 50.17
Duration      [total, attack, wait]             4.983s, 4.981s, 2.429ms
Latencies     [min, mean, 50, 90, 95, 99, max]  732.927µs, 2.92ms, 2.468ms, 3.575ms, 5.363ms, 26.182ms, 39.539ms
Bytes In      [total, mean]                     234500, 938.00
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:250  
Error Set:

Bun.serve

Bun プロセスで実行させるため、Bunに内臓されているhttpサーバー(Bun.server)で代用します。 expressで使えるserve-staticみたいなのがないか探していたところ良さげなものを見つけたのでこちらを利用しようと思います。 github.com

ちなみに、上記のexpressのコードを実行するとserve-staticがBun.shでは動きませんでした(おそらくバグ?)。 Bun.serve用のサーバーコード

import serveStatic from "serve-static-bun";
Bun.serve({ fetch: serveStatic("public"), port: 3000 });

実行コマンド

bun server_bun.js

計測結果

$ echo 'GET http://localhost:3000' | vegeta attack -output=./bun_report.bin -duration=5s | vegeta report ./bun_report.bin
Requests      [total, rate, throughput]         250, 50.21, 50.20
Duration      [total, attack, wait]             4.98s, 4.979s, 1.594ms
Latencies     [min, mean, 50, 90, 95, 99, max]  577.129µs, 1.439ms, 1.402ms, 1.735ms, 1.921ms, 2.592ms, 3.517ms
Bytes In      [total, mean]                     234500, 938.00
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:250  
Error Set:

テキストで表示されてもちょっと見づらいのでプロットにします。

vegeta plot ./node_report.bin > node_prot.html
vegeta plot ./bun_report.bin > bun_prot.html

左右に並べて見ると、平時は同じくらいですが、負荷が高くなるとnodeの方がlatencyが高く、Bunの方がはやいことがわかります。 左側がBun.serve、右側がnode + express

bun、nodeの比較
bun、nodeの比較

まとめ

  • バグなのかnpm互換性が足りなく動かないことがある
  • moduleのインストールは爆速になる
  • サーバーサイドレンダリング&ページ初期表示は少し早くなる
  • bun runで実行したコマンドはあまり速さは変わらない

moduleのインストール速度は爆速なので、新規参入タイミングでのコストは下がりそうですね。
また、大規模なクライアント開発なら速度改善の恩恵が受けられそう。
まだ出たてで、バグで動かないモジュールなども結構あるので、成熟するまで今後の動向を暖かく見守っていきたいですね。