なぜランタイムを検証するのか?
なんとなく知っていたが、理解を深めたかった
現在ジョインしている会社のプロダクトではNodeとBunを使用しています。
正直雰囲気で使っていたので、少し理解を深めたかったのです。
本当にBunは速いのか?
公式の情報では「BunはNode.jsよりも速い」とされるが、具体的な処理の差を確認したかったです。
サーバーサイドの負荷を理解したい
最近NextなどでAPIサーバーを建てずフロントエンドだけで完結することが増えてきて、フロントエンドとバックエンドの境界が曖昧になってきてます。
そうなったとき、ページを動的に生成するためにCPU負荷や非同期処理の最適化が重要になり、パフォーマンスがランタイムの影響を大きくのではと思ったからです。
JSのランタイムとは?
JSの実行環境です。JSは本来ブラウザのJSエンジンを使って実行されますが。
Node.jsなどのランタイムをつかってサーバーサイドでファイル操作・DB操作などを行うことができます。
ランタイム | 特徴 |
---|---|
Node.js | 豊富な実績とエコシステム、npmの強力なパッケージ管理 |
Deno | セキュリティ重視、ESModulesネイティブ対応 |
Bun | 統合ツールチェーンを備え、高速な実行速度とnpm互換性 |
ランタイムが重要な理由
- CPU負荷:SSRではページ生成時に計算処理が発生。
-
非同期処理:
Promise
の処理速度の違い。 - I/O処理:ログ出力やキャッシュ管理などのファイル読み書き。
- メモリ管理:サーバーのメモリ消費がスケーラビリティに影響。
より効率の良いランタイムを選択すれば、SSRのパフォーマンス向上やスケーラビリティの改善 につながる。
最近のアーキテクチャのトレンド
フロントエンドとバックエンドの境界が曖昧になってきてます。
これまで、Web開発におけるフロントエンドとバックエンドの役割は明確に分かれていました。
従来のアーキテクチャの変遷を振り返ると、以下のような流れが見えてきます。
1 サーバーサイドレンダリング (SSR) の時代(2000年代)
- PHP, Ruby on Rails, Django などのサーバーサイドフレームワークを使用し、サーバー側でHTMLを生成してレスポンスを返す のが主流でした。
2 SPA(Single Page Application)の台頭(2010年代)
- Angular, React, Vue.js などのフレームワークが普及し、クライアントサイドでページを動的に構築する スタイルが一般的になりました。
- RailsやLalabelなどのMVCフレームワークでもフロントエンドはできますが、上述したJSフレームワークには劣ります。
ですのでAPI (REST, GraphQL) を用いてフロントエンドとバックエンドを完全に分離するアーキテクチャが流行しました。
3 再びSSRの復権(2020年代~)
- SPAの初回表示の遅さ やSEOの課題 が浮上し、Next.js, Nuxt.js などのSSR対応フレームワークが注目されるようになりました。
- バックエンドの役割がNext.jsなどのフロントエンドフレームワークの発展によりj,統合される流れが加速していってます。
検証方法
測定項目
- CPU負荷:ループ処理の速度(HTMLの動的生成を想定)
-
非同期処理:
Promise
の解決速度 - I/O処理:ファイルの読み書き速度
- メモリ消費:大量のオブジェクトを生成した際のヒープ増加量
- ホットリロード:開発中のリアルタイム反映速度
-
ビルド時間:Next.jsの
build
実行時間 - パッケージインストール時間:package.jsonからライブラリを取得する実行時間
検証環境
- MacBook Pro M3
- Next.js 15.1.6
- Node.js (npm) vs Bun
-
GET /test
でAPIの応答時間計測
Nextのapiディレクトリ配下には下記のようなファイルを置き、localで検証しました。
例えばCPU検証なら下記のようなエンドポイントに対してChromeで叩いて検証しました
http://localhost:3000/api/benchmark?type=cpu
// app/api/benchmark/route.ts
import { NextResponse } from 'next/server';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
export async function GET(request: Request) {
// URL のクエリパラメータから、どのテストを実行するか取得(デフォルトは 'cpu')
const { searchParams } = new URL(request.url);
const type = searchParams.get('type') || 'cpu';
if (type === 'cpu') {
// CPUベンチマーク:1億回のループ処理
const start = process.hrtime.bigint();
let sum = 0;
for (let i = 0; i < 1e8; i++) {
sum += i;
}
const end = process.hrtime.bigint();
const elapsed = Number(end - start) / 1e6; // ミリ秒に変換
return NextResponse.json({
test: 'cpu',
sum,
elapsedTime: elapsed.toFixed(2) + ' ms',
});
} else if (type === 'async') {
// 非同期処理ベンチマーク:100万回、Promise を解決
const start = performance.now();
let value = 0;
for (let i = 0; i < 1e6; i++) {
value = await Promise.resolve(i);
}
const end = performance.now();
return NextResponse.json({
test: 'async',
lastValue: value,
elapsedTime: (end - start).toFixed(2) + ' ms',
});
} else if (type === 'io') {
// I/Oベンチマーク:一時ファイルへの書き込み・読み込み
const tmpFile = path.join(os.tmpdir(), 'benchmark.txt');
const data = 'Hello, World!\n';
const iterations = 1000;
const start = process.hrtime.bigint();
for (let i = 0; i < iterations; i++) {
fs.writeFileSync(tmpFile, data, { flag: 'a' });
}
// 読み込み(オプション)
fs.readFileSync(tmpFile, 'utf8');
const end = process.hrtime.bigint();
const elapsed = Number(end - start) / 1e6;
// クリーンアップ
fs.unlinkSync(tmpFile);
return NextResponse.json({
test: 'io',
iterations,
elapsedTime: elapsed.toFixed(2) + ' ms',
});
} else if (type === 'memory') {
// メモリベンチマーク:大量のオブジェクトを生成して、メモリ使用量の増加を測定
const startMemory = process.memoryUsage().heapUsed;
const arr = [];
for (let i = 0; i < 1e6; i++) {
arr.push({ index: i, data: new Array(100).fill('x') });
}
const endMemory = process.memoryUsage().heapUsed;
return NextResponse.json({
test: 'memory',
startMemory,
endMemory,
diffMemory: endMemory - startMemory,
});
} else {
return NextResponse.json({ error: 'Unknown benchmark type' });
}
}
検証結果
項目 | npm (Node.js) | Bun | 備考 |
---|---|---|---|
CPU負荷 | 77.32 ms | 88.29 ms | ほぼ同等 |
非同期処理 | 357.40 ms | 342.69 ms | Bunが若干速い |
I/O処理 | 17.95 ms | 18.12 ms | 同じくらい |
メモリ消費 | ヒープ増加:約902 MB | ヒープ増加:約898 MB | ほぼ同じ |
ホットリロード | 初回: 1349 msその後: 15~56 ms | 初回: 1160 msその後: 12~39 ms | Bunが高速 |
ビルド時間の比較
ランタイム | ビルド時間 |
---|---|
npm | 6.043秒 |
Bun | 6.133秒 |
パッケージインストール時間の比較
ランタイム | ビルド時間 |
---|---|
npm | 12秒 |
Bun | 4秒 |
考察
検証の結果、Bunの一部の処理はNode.jsよりも速いものの、SSRの実行性能に大きな差はないことがわかりました。
わたしの検証したプロダクトはCRUDができるようになったようなくらいの小規模なアプリのため、より大規模な処理が必要になったり、依存関係が複雑になっていくと差が出やすくなるのかもしれません。
逆に言うと、小規模プロダクトでランタイムの差はそこまで差は出ないといえるでしょう。
Node.js vs Bunの違い
-
処理性能はほぼ同じ
- CPU負荷、非同期処理、I/O、メモリ管理の面では、大きな違いはない。
-
Bunの方が開発体験が良い
- ホットリロードの速度(初回のみ)やパッケージ管理の速さはBunの強み。
-
ビルド時間の違いは誤差レベル
- SSRでのビルド時間も大きな違いはない。
結論
実行速度の面では、小規模プロダクトであれば、どちらを選んでもほとんど同じなので問題ない。
インストールなどの速さなど、Bunのほうが開発体験がよいので、Bunに軍配が上がる。
あえて欠点を上げるとすれば、再現できないのだが、Bunで新しいパッケージをインストールしたとき、若干不安定な時がある。
まとめ
Next.jsなどのフレームワークの発展により、バックエンドなしでフロントエンドが完結する流れが強まっている。その中でランタイムの選択が重要になっているが、Node.jsとBunの実行速度はほぼ同じ。
なので、開発体験を重視するならBunが優位になる。