echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

JSConf JP 2023の「Bunがメジャーリリースされたけど、本当にBunはNode.jsに取って代るほどすごいのか?をAWS Lambdaで検証してみた」で説明しきれなかった点の補足

はじめに

この記事は、Bun Advent Calendar 2023 4日目の記事です。

qiita.com

JSConf JP 2023について

先月の11/19(日) に「JSConf JP」という、JavaScriptの一大Festivalが開催されました。

jsconf.jp

そしてその中で「Bunがメジャーリリースされたけど、本当にBunはNode.jsに取って代るほどすごいのか?をAWS Lambdaで検証してみた」という(長いタイトルの)内容で登壇させて頂きました。

※聞いてくださった方、ありがとうございました!

jsconf.jp

今回のJSConf JPのセッションではかなりレア(もしかしたら唯一?)のAWS & 100% バックエンドの内容だったので、どうかなあという感じでしたが、結果的には多くの方が聞きに来てくれて、良かったという感じでした。

あとは本番の魔物(接続トラブル)さえなければ...

で、今回はそのJSConf JPのセッションで話しきれなかったことについての話になります。

ちなみに、発表資料はこちらになります。

speakerdeck.com

アジェンダ

  • 結局、Bunって実際の実行速度はそうでもないの?
  • 「Bun(Node.jsビルド)」って、何?
  • 「TypeScriptで動かす」の補足
  • 「ビルドファイルが動かない現象を回避する方法」の補足

結局、Bunって実際の実行速度はそうでもないの?

資料の中で「AWS Lambda(以下Lambda)ではBunは思ったほど早くなかった」という検証結果を話しました。

それについて、発表後に「Webサーバーとかではどうですか」とか「Webサーバーとしては早いですよ」という意見を頂きました。

そこで調べたところ、確かにnode.js ネイティブやExpressではBunの方が速いという結果が出ているようです。

ただし、Fastifyではnode.jsの方が速いようです。

また他にも「Bunの方が速い」という結果を出していた海外の比較サイトもありました。(サイトは失念...)

なので、結論から言うと「環境次第」ということなんでしょう。(自分がフロントは専門外なので、あまり突っ込んで調査はしていない)

ただBun自体、まだ9/8にメジャーリリースされたばかりですし、Bun公式サイト でトップページのトップでサーバーサイドレンダリングベンチマーク結果を表示していることからも、Webサーバーとしての速度は今後どんどん速くなっていくんでしょうね。

「Bun(Node.jsビルド)」って、何?

上記「検証結果」の項目の中で「Bun(Node.jsビルド)」という項目があります。

それについて「『Bun(Node.jsビルド)』って、何?」という質問がありましたので、改めてここで記載します。

Bunにはビルドターゲットとして以下の3つがあり、それぞれビルド結果が異なります。(browserは今回は検証から除外)

  • Bun: Bunランタイムに最適な形式でビルドされる
  • node: Node.jsに最適な形式でビルドされる
  • browser:Webブラウザで動作させるのに最適な形式でビルドされる

参考:Bun.Build

そして「Bun(Node.jsビルド)」は上記のターゲット:nodeでビルドした場合の結果になります。

ちなみにBun公式のbun-lambdaパッケージにも記載がある通り、BunをLambdaで動かす場合、API GatewayのeventはRequest形式に変換されます。

「TypeScriptで動かす」の補足

BunでLambdaを動かすことのメリットとして「TypeScriptのままLambdaにアップロードできる」という事を挙げました。
これに関する補足です。

具体的には、下記の手順を踏むことでBunでTypeScriptのままLambdaを実行する事が出来ます。

  1. Bun用のLambda Layerを作成する(これは先述のbun-lambdaパッケージで作成可能)
  2. Lambdaで使用するnpmモジュールを別途Lambda Layerとして作成する
  3. TypeScriptでLambdaを記載する
  4. 該当のLambda関数の設定で、1~2のLambda Layerを使用する設定を行う

ただし、下記のデメリットが発生するので注意です。

  • Lambdaの実行時間が長くなる(これは資料内でも説明した通り)
  • これだけでLambda Layerを2つ使う
    • Lambda Layerは最大5つまで。(あまり意識する必要はないかも?)

「ビルドファイルが動かない現象の回避方法」の補足

また発表内で、BunをLambdaで動かす際の注意点として、下記を挙げました。

  • node_modulesを使用すると、ビルド後のjs ファイル実行時に「(intermediate value).require is not a function 」エラーが発生する
  • ビルド後のjsファイルの先頭に下記2行を追加することで回避可能
import { createRequire as createImportMetaRequire } from "module";
import.meta.require ||= (id) => createImportMetaRequire(import.meta.url)(id);

これについて、具体的には下記の手順で実行します。

  1. Bunのビルド設定ファイル (build.ts) を用意する
  2. build.ts に下記ソースを記載する
  3. Bunのビルド時に、下記コマンドのように build.ts を使用してビルドするようにする。
bun run build.ts
// build.tsの内容
import path from "node:path";
import process from "node:process";
import fs from "node:fs";
import { Transform, TransformCallback } from 'stream'
  
// ビルドするファイルの設定。
// 最終的にビルドファイルのパスが取れればOK
// ここでは1ファイルのみだが、もちろん複数ファイルでもOK
const projectBaseDir = process.cwd();
  
const input_bun = path.resolve(
  projectBaseDir,
  "lambda/index_bun.ts"
);
  
// ビルドファイルの出力先&拡張子設定
const output = path.resolve(projectBaseDir, "dist");
const ext = "mjs";
  
// targetはbunまたはnodeを指定
await Bun.build({
  entrypoints: [input_bun],
  outdir: output,
  target: "bun",
  format: "esm",
  naming: `[dir]/[name].${ext}`
});
  
// ビルドされたファイルを1つずつ読み込む。
// ビルドファイル名.bakという一時ファイルを作成する
// Stream形式にしているのは、ビルドされたファイルのサイズが
// 大きくてもOOMで落ちないようにするため
const pathToBuildedFileBun = path.resolve(projectBaseDir, `dist/index_bun.${ext}`);
const files = [pathToBuildedFileBun]
  
for (const file of files) {
  const inputFile = fs.createReadStream(file, {
    encoding: "utf-8"
  });
  const outputFile = fs.createWriteStream(`${file}.bak`, {
    encoding: "utf-8"
  });
  
  // .bakファイルの先頭に回避用コードを記載
  outputFile.write('import { createRequire as createImportMetaRequire } from "module"; import.meta.require ||= (id) => createImportMetaRequire(import.meta.url)(id);\n\n')
  
  // あとはビルドファイルのコードをそのままコピー
  const decoder = new TextDecoder();
  const transformer = new Transform({
    transform(
      chunk: Uint8Array, 
      encoding: string, 
      done: TransformCallback
    ): void {
      let chunkString = decoder.decode(chunk);

      this.push(chunkString) // 加工処理
      done()
    },
  })
  
  // https://qiita.com/suin/items/8bf63cd457d75b709530
  // https://qiita.com/masakura/items/5683e8e3e655bfda6756
  inputFile.pipe(transformer).pipe(outputFile);
  
  // 最後に元のビルドファイルを削除して、.bakファイルの
  // ファイル名を元のビルドファイル名にリネーム
  fs.unlinkSync(file);
  fs.renameSync(`${file}.bak`, file);
}

まとめ

以上、JSConf JPで説明しきれなかった点の補足でした。

今回の発表で、始めてBunを本格的に触ってみましたが、パッケージインストール・ビルド・テストなど、ローカル作業の速度に関してはかなり速く、とても魅力的でした。

また本番稼働させるには色々と壁がありますが、先述の通り、まだまだメジャーリリースされたばかりなので、今後の進化に期待ですね。

それでは、今回はこの辺で。
明日の Bun Advent Calendar 2023 もお楽しみに!