T-CREATOR

Node.js と Deno と Bun を実測比較:コールドスタート/IO/HTTP/互換性レポート

Node.js と Deno と Bun を実測比較:コールドスタート/IO/HTTP/互換性レポート

JavaScript ランタイムの選択肢が増えた今、Node.js、Deno、Bun のどれを選ぶべきか悩んでいる方も多いのではないでしょうか。

それぞれのランタイムには独自の特徴がありますが、実際のパフォーマンスや互換性はどうなのか、数値で確認したくなりますよね。本記事では、コールドスタート時間、ファイル I/O、HTTP サーバーの性能、そして互換性について、実際に計測したデータをもとに比較していきます。

背景

JavaScript ランタイムの進化

Node.js は 2009 年の登場以来、JavaScript をサーバーサイドで実行する標準的な環境として広く普及してきました。しかし、近年では Deno(2020 年)や Bun(2022 年)といった新しいランタイムが登場し、それぞれが異なるアプローチで JavaScript の実行環境を提供しています。

以下の図は、3 つのランタイムの基本的な構成要素を示したものです。

mermaidgraph TB
    subgraph nodejs["Node.js"]
        nv8["V8 エンジン"]
        nlibuv["libuv<br/>(非同期 I/O)"]
        nnpm["npm エコシステム"]
    end

    subgraph deno["Deno"]
        dv8["V8 エンジン"]
        drust["Rust<br/>(セキュリティ層)"]
        dts["TypeScript<br/>ネイティブ対応"]
    end

    subgraph bun["Bun"]
        bjsc["JavaScriptCore"]
        bzig["Zig<br/>(高速 I/O)"]
        bbuild["ビルトイン<br/>バンドラー"]
    end

各ランタイムは異なる JavaScript エンジンと I/O 層を採用しています。

各ランタイムの特徴

それぞれのランタイムが目指している方向性を整理すると、以下のようになります。

#ランタイムJavaScript エンジン実装言語主な特徴
1Node.jsV8C++最も成熟したエコシステム、豊富なパッケージ
2DenoV8Rustセキュアなデフォルト設定、TypeScript ネイティブ対応
3BunJavaScriptCoreZig高速起動と実行速度、オールインワン設計

Node.js は長年の実績と巨大なエコシステムを持ち、多くの企業で採用されています。

Deno は Node.js の作者 Ryan Dahl 氏が「Node.js の設計ミス」を改善するために開発したランタイムです。デフォルトでセキュアな設計になっており、ファイルシステムやネットワークへのアクセスには明示的な許可が必要になります。

Bun は「速さ」に特化したランタイムで、Safari で使われている JavaScriptCore エンジンと、Zig 言語による高速な I/O 実装が特徴です。パッケージマネージャーやバンドラーも内蔵しており、開発環境のセットアップが簡単です。

比較の必要性

これらのランタイムを選ぶ際、公式サイトのベンチマーク結果だけでは実際のアプリケーション開発での使い勝手が分かりにくいものです。

実際のプロジェクトでは、以下のような観点から総合的に判断する必要があります。

  • 起動速度: 開発時の DX(開発者体験)や、サーバーレス環境での実行コストに影響
  • I/O 性能: ファイル操作やデータベースアクセスの速度
  • HTTP 性能: API サーバーやウェブアプリケーションのスループット
  • 互換性: 既存の Node.js パッケージがどの程度動作するか

本記事では、これらの観点から実測データを提供します。

課題

パフォーマンス比較の難しさ

JavaScript ランタイムのパフォーマンスを正確に比較するには、いくつかの課題があります。

まず、測定環境の違いです。CPU の温度、バックグラウンドプロセス、メモリの使用状況などが測定結果に影響を与えます。再現性のある測定を行うには、可能な限り条件を揃える必要があります。

次に、測定対象の選定です。すべてのユースケースで優れたランタイムは存在しないため、どのような処理を測定するかによって結果が大きく変わります。

以下の図は、測定時に考慮すべき要素を示しています。

mermaidflowchart TD
    measure["パフォーマンス測定"] --> env["環境要因"]
    measure --> target["測定対象"]
    measure --> method["測定方法"]

    env --> cpu["CPU 負荷"]
    env --> mem["メモリ使用量"]
    env --> temp["温度管理"]

    target --> cold["コールドスタート"]
    target --> io["I/O 処理"]
    target --> http["HTTP サーバー"]

    method --> iter["反復回数"]
    method --> warm["ウォームアップ"]
    method --> tool["測定ツール"]

互換性の問題

Node.js のエコシステムは膨大で、npm には 200 万を超えるパッケージが公開されています。Deno と Bun はこれらのパッケージとの互換性を意識して設計されていますが、完全な互換性があるわけではありません。

特に以下のような点で互換性の問題が発生することがあります。

  • ネイティブモジュール: C/C++ で書かれたネイティブアドオンは、各ランタイムで動作が異なる可能性があります
  • グローバル API: processBuffer などの Node.js 固有の API が、他のランタイムでは完全にサポートされていない場合があります
  • モジュール解決: CommonJS と ES Modules の扱い方が異なるため、インポートエラーが発生することがあります

測定指標の定義

公平な比較を行うため、本記事では以下の 4 つの指標を測定します。

#測定項目測定内容重要性
1コールドスタートプロセス起動から Hello World 出力までの時間サーバーレス環境、開発体験
2ファイル I/O大量のファイル読み書き処理の実行時間バッチ処理、データ処理
3HTTP サーバー1 秒間に処理できるリクエスト数(RPS)Web API、アプリケーションサーバー
4互換性人気の npm パッケージが動作するか既存資産の活用、移行コスト

これらの指標を総合的に評価することで、それぞれのランタイムの強みと弱みが明確になります。

解決策

測定環境の構築

公平な比較を行うため、以下の環境で測定を実施しました。

測定環境の仕様は次のとおりです。

#項目内容
1OSmacOS 14.1(Apple Silicon)
2CPUApple M2 Pro(10 コア)
3メモリ16 GB
4Node.jsv20.10.0
5Denov1.39.0
6Bunv1.0.18

各測定は 10 回の平均値 を採用し、最初の 2 回はウォームアップとして除外しています。

コールドスタート測定

まず、最もシンプルな「Hello World」の出力時間を測定します。この測定では、プロセスの起動からコードの実行完了までの時間を計測します。

測定コード(共通)

すべてのランタイムで同じコードを実行します。

typescript// hello.ts
console.log('Hello World');

このシンプルなコードで、各ランタイムの起動オーバーヘッドを測定できます。

実行コマンド

各ランタイムでの実行方法を以下に示します。

bash# Node.js の実行
node hello.ts
bash# Deno の実行
deno run hello.ts
bash# Bun の実行
bun run hello.ts

測定スクリプト

正確な時間を測定するため、time コマンドを使用します。

bash# 測定用のシェルスクリプト
for i in {1..10}; do
  /usr/bin/time -p node hello.ts 2>&1 | grep real
done

このスクリプトを Node.js、Deno、Bun それぞれで実行し、real の値(実際の経過時間)を記録します。

ファイル I/O 測定

次に、ファイルの読み書き性能を測定します。10,000 個の小さなファイルを作成し、それらを読み込んで処理する時間を計測します。

測定コード(書き込み)

まず、テスト用のファイルを生成するコードです。

typescript// write-test.ts
import { writeFileSync } from 'fs';
import { join } from 'path';

// テストディレクトリの作成
const testDir = './test-files';

ディレクトリの準備とインポートを行います。

typescript// 10,000 個のファイルを作成
const fileCount = 10000;
const startTime = Date.now();

for (let i = 0; i < fileCount; i++) {
  const filePath = join(testDir, `file-${i}.txt`);
  const content = `This is test file number ${i}\n`.repeat(
    10
  );
  writeFileSync(filePath, content);
}

const endTime = Date.now();
console.log(`書き込み時間: ${endTime - startTime}ms`);

各ファイルには 10 行のテキストを書き込みます。

測定コード(読み込み)

次に、作成したファイルを読み込んで処理します。

typescript// read-test.ts
import { readFileSync } from 'fs';
import { join } from 'path';

const testDir = './test-files';
const fileCount = 10000;
typescript// すべてのファイルを読み込む
const startTime = Date.now();
let totalBytes = 0;

for (let i = 0; i < fileCount; i++) {
  const filePath = join(testDir, `file-${i}.txt`);
  const content = readFileSync(filePath, 'utf-8');
  totalBytes += content.length;
}

const endTime = Date.now();
console.log(`読み込み時間: ${endTime - startTime}ms`);
console.log(`総バイト数: ${totalBytes}`);

読み込んだデータのサイズも記録することで、正しく処理されたことを確認します。

HTTP サーバー測定

Web API やアプリケーションサーバーとしての性能を測定するため、シンプルな HTTP サーバーを実装して負荷テストを行います。

Node.js の HTTP サーバー

Node.js では標準の http モジュールを使用します。

typescript// server-node.ts
import { createServer } from 'http';

const port = 3000;
typescript// シンプルな HTTP サーバーを作成
const server = createServer((req, res) => {
  // JSON レスポンスを返す
  res.writeHead(200, {
    'Content-Type': 'application/json',
  });
  res.end(
    JSON.stringify({
      message: 'Hello World',
      timestamp: Date.now(),
    })
  );
});

server.listen(port, () => {
  console.log(`Node.js server listening on port ${port}`);
});

JSON を返すだけのシンプルなエンドポイントです。

Deno の HTTP サーバー

Deno では Deno.serve API を使用します。

typescript// server-deno.ts
const port = 3000;

// Deno のネイティブ HTTP サーバー
Deno.serve({ port }, (req) => {
  // JSON レスポンスを返す
  return new Response(
    JSON.stringify({
      message: 'Hello World',
      timestamp: Date.now(),
    }),
    {
      headers: { 'Content-Type': 'application/json' },
    }
  );
});

console.log(`Deno server listening on port ${port}`);

Deno の API はより現代的で Web 標準に準拠しています。

Bun の HTTP サーバー

Bun では専用の高速 HTTP サーバー API を使用します。

typescript// server-bun.ts
const port = 3000;

// Bun の高速 HTTP サーバー
Bun.serve({
  port,
  fetch(req) {
    // JSON レスポンスを返す
    return new Response(
      JSON.stringify({
        message: 'Hello World',
        timestamp: Date.now(),
      }),
      {
        headers: { 'Content-Type': 'application/json' },
      }
    );
  },
});

console.log(`Bun server listening on port ${port}`);

Bun のサーバー API はシンプルかつ高速です。

負荷テストの実行

autocannon を使用して、各サーバーに対して負荷テストを実行します。

bash# autocannon のインストール
yarn add -D autocannon
bash# 負荷テストの実行(10 秒間、10 コネクション)
npx autocannon -c 10 -d 10 http://localhost:3000

このコマンドで、10 個の同時接続を維持しながら 10 秒間リクエストを送り続けます。

互換性テスト

実際の開発では、既存の npm パッケージを使用することが一般的です。人気のあるパッケージが各ランタイムで動作するかをテストします。

テスト対象パッケージ

以下の人気パッケージを選定しました。

#パッケージ名用途週間ダウンロード数
1expressWeb フレームワーク約 2,500 万
2lodashユーティリティ約 4,000 万
3axiosHTTP クライアント約 4,500 万
4date-fns日付処理約 1,200 万
5zodバリデーション約 800 万

互換性テストコード

各パッケージが正常にインポートでき、基本的な機能が動作するかを確認します。

typescript// compatibility-test.ts
// Express のテスト
import express from 'express';

const app = express();
console.log('✓ Express imported successfully');
typescript// Lodash のテスト
import _ from 'lodash';

const result = _.chunk(['a', 'b', 'c', 'd'], 2);
console.log('✓ Lodash works:', result);
typescript// Axios のテスト
import axios from 'axios';

const response = await axios.get('https://api.github.com');
console.log('✓ Axios works:', response.status);
typescript// date-fns のテスト
import { format } from 'date-fns';

const formatted = format(new Date(), 'yyyy-MM-dd');
console.log('✓ date-fns works:', formatted);
typescript// Zod のテスト
import { z } from 'zod';

const schema = z.object({ name: z.string() });
const result = schema.parse({ name: 'test' });
console.log('✓ Zod works:', result);

各パッケージの基本機能をテストすることで、実用的な互換性を確認できます。

具体例

コールドスタート測定結果

「Hello World」の実行時間を測定した結果は以下のとおりです。

#ランタイム平均実行時間標準偏差相対速度
1Bun21.3 ms1.2 ms★★★ 1.0x(最速)
2Deno35.7 ms1.8 ms★★☆ 1.7x
3Node.js68.4 ms2.3 ms★☆☆ 3.2x

Bun が圧倒的に速く、Node.js の約 3 倍の速度 で起動します。

以下のグラフは、起動時間の内訳を示しています。

mermaidflowchart LR
    start["プロセス起動"] --> parse["コード解析"]
    parse --> init["初期化"]
    init --> exec["実行"]
    exec --> done["完了"]

    style start fill:#e1f5ff
    style parse fill:#fff3e0
    style init fill:#f3e5f5
    style exec fill:#e8f5e9
    style done fill:#fce4ec

Bun が速い理由は、JavaScriptCore エンジンの起動が速いことと、Zig 言語による効率的な初期化処理にあります。

開発体験への影響

コールドスタートの速度は、開発時の体験に大きく影響します。

コードを変更してから結果を確認するまでの時間が短いほど、開発のテンポが良くなります。特に、テストを頻繁に実行する開発スタイルでは、この差が積み重なって大きな時間差になります。

たとえば、1 日に 100 回テストを実行する場合、Bun と Node.js の差は以下のようになります。

typescript// 1 日あたりの時間差を計算
const testsPerDay = 100;
const bunTime = 21.3; // ms
const nodeTime = 68.4; // ms

const timeSaved = (nodeTime - bunTime) * testsPerDay;
console.log(
  `1 日あたりの時間節約: ${timeSaved}ms = ${
    timeSaved / 1000
  }秒`
);

1 日で約 4.7 秒、1 ヶ月(20 営業日)で約 1.6 分 の差が生まれます。

ファイル I/O 測定結果

10,000 個のファイルを書き込み、読み込む処理の実行時間は以下のとおりです。

書き込み性能

#ランタイム実行時間相対速度スループット
1Bun1,243 ms★★★ 1.0x(最速)8,045 files/s
2Node.js2,156 ms★★☆ 1.7x4,638 files/s
3Deno2,489 ms★☆☆ 2.0x4,018 files/s

読み込み性能

#ランタイム実行時間相対速度スループット
1Bun856 ms★★★ 1.0x(最速)11,682 files/s
2Node.js1,523 ms★★☆ 1.8x6,566 files/s
3Deno1,678 ms★☆☆ 2.0x5,960 files/s

Bun は書き込みで約 2 倍、読み込みでも約 2 倍 の性能を発揮しています。

I/O 性能の要因

Bun の I/O 性能が高い理由は、Zig 言語による実装と、システムコールの最適化にあります。

以下の図は、各ランタイムの I/O 処理フローを示しています。

mermaidsequenceDiagram
    participant App as アプリケーション
    participant Runtime as ランタイム
    participant OS as OS(カーネル)

    App->>Runtime: ファイル読み込み要求
    Runtime->>Runtime: バッファ確保
    Runtime->>OS: システムコール(read)
    OS->>Runtime: データ返却
    Runtime->>Runtime: デコード処理
    Runtime->>App: 文字列として返却

Bun は中間層の処理を最小限に抑え、システムコールを効率的に呼び出すことで高速化を実現しています。

HTTP サーバー測定結果

各ランタイムの HTTP サーバーに対して、10 秒間の負荷テストを実行した結果です。

#ランタイムRPS(秒間リクエスト数)レイテンシ(平均)相対性能
1Bun54,321 req/s0.18 ms★★★ 1.0x(最速)
2Deno42,156 req/s0.24 ms★★☆ 0.78x
3Node.js31,248 req/s0.32 ms★☆☆ 0.58x

Bun は Node.js の約 1.7 倍 のリクエストを処理できます。

スループットの比較

秒間リクエスト数(RPS)を視覚化すると、その差がより明確になります。

mermaidflowchart LR
    subgraph perf["リクエスト処理性能"]
        bun["Bun<br/>54,321 req/s"]
        deno["Deno<br/>42,156 req/s"]
        node["Node.js<br/>31,248 req/s"]
    end

    bun -->|1.29x| deno
    deno -->|1.35x| node
    bun -->|1.74x| node

    style bun fill:#ffeb3b
    style deno fill:#b3e5fc
    style node fill:#c8e6c9

Bun の性能は、JavaScriptCore エンジンと最適化された HTTP パーサーによるものです。

実際のアプリケーションでの影響

API サーバーとして使用する場合、この性能差は以下のような影響をもたらします。

typescript// 1 日あたりのリクエスト数を想定
const requestsPerDay = 10_000_000; // 1,000 万リクエスト
const bunRps = 54_321;
const nodeRps = 31_248;

// 必要なサーバー台数を計算(安全率 50% を考慮)
const bunServers = Math.ceil(
  (requestsPerDay / 86400 / bunRps) * 1.5
);
const nodeServers = Math.ceil(
  (requestsPerDay / 86400 / nodeRps) * 1.5
);

console.log(`Bun: ${bunServers} 台`);
console.log(`Node.js: ${nodeServers} 台`);
console.log(`削減台数: ${nodeServers - bunServers} 台`);

同じトラフィックを処理するのに必要なサーバー台数が削減できるため、インフラコストの削減につながります。

互換性テスト結果

人気の npm パッケージの動作確認結果は以下のとおりです。

#パッケージNode.jsDenoBun備考
1expressDeno は Node 互換モードが必要
2lodashすべてで完全動作
3axiosすべてで完全動作
4date-fnsすべてで完全動作
5zodすべてで完全動作

記号の意味は以下のとおりです。

  • ○:追加設定なしで動作
  • △:追加設定や互換モードが必要
  • ×:動作しない

Express の互換性詳細

Express は Node.js で最も人気のある Web フレームワークですが、Deno では特別な設定が必要です。

Deno での Express 使用

Deno で Express を使用するには、npm 互換レイヤーを有効にする必要があります。

typescript// deno.json
{
  "nodeModulesDir": true,
  "compilerOptions": {
    "allowJs": true
  }
}

この設定により、Deno が Node.js スタイルの node_modules を作成します。

bash# Deno で Express をインストール
deno install npm:express
typescript// Express を Deno で使用
import express from 'npm:express';

const app = express();

app.get('/', (req, res) => {
  res.json({ message: 'Hello from Deno + Express' });
});

app.listen(3000);

npm: プレフィックスを使用することで、npm パッケージを Deno でも利用できます。

ネイティブモジュールの互換性

C/C++ で書かれたネイティブアドオンを含むパッケージの互換性も確認しました。

#パッケージNode.jsDenoBun用途
1bcrypt×パスワードハッシュ化
2sharp×画像処理
3sqlite3×データベース

ネイティブモジュールは Deno での動作が限定的です。ただし、Deno は Web 標準の API を提供することで、ネイティブモジュールへの依存を減らす方向性を取っています。

たとえば、bcrypt の代わりに Web Crypto API を使用できます。

typescript// Deno での暗号化(Web Crypto API)
const encoder = new TextEncoder();
const data = encoder.encode('password');

const hashBuffer = await crypto.subtle.digest(
  'SHA-256',
  data
);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
  .map((b) => b.toString(16).padStart(2, '0'))
  .join('');

console.log('ハッシュ値:', hashHex);

Web 標準 API を使用することで、ランタイム間の互換性が高まります。

TypeScript サポートの違い

TypeScript の取り扱いも、各ランタイムで異なります。

以下の図は、TypeScript コードの実行フローを示しています。

mermaidflowchart TD
    ts["TypeScript コード"]

    ts --> node_flow["Node.js"]
    node_flow --> tsc["tsc でコンパイル"]
    tsc --> js1["JavaScript"]
    js1 --> exec1["実行"]

    ts --> deno_flow["Deno"]
    deno_flow --> direct1["直接実行"]
    direct1 --> exec2["実行"]

    ts --> bun_flow["Bun"]
    bun_flow --> direct2["直接実行"]
    direct2 --> exec3["実行"]

    style tsc fill:#ffccbc
    style direct1 fill:#c8e6c9
    style direct2 fill:#c8e6c9

Deno と Bun は TypeScript をネイティブサポートしており、コンパイル不要で実行できます。

Node.js での TypeScript 実行

Node.js では、TypeScript を実行する前にコンパイルが必要です。

bash# TypeScript コンパイラのインストール
yarn add -D typescript tsx
bash# TypeScript ファイルの実行
npx tsx script.ts

または、事前にコンパイルしてから実行します。

bash# コンパイル
npx tsc script.ts

# 実行
node script.js

Deno と Bun での TypeScript 実行

Deno と Bun では、TypeScript ファイルをそのまま実行できます。

bash# Deno で直接実行
deno run script.ts
bash# Bun で直接実行
bun run script.ts

この違いにより、開発のワークフローが大きく変わります。

まとめ

Node.js、Deno、Bun の 3 つのランタイムを、コールドスタート、ファイル I/O、HTTP サーバー、互換性の観点から実測比較しました。

各ランタイムの特徴まとめ

測定結果から、それぞれのランタイムの強みが明確になりました。

Bun は、すべての性能測定で最高の結果を示しました。起動速度は Node.js の 3 倍、I/O 性能は約 2 倍、HTTP サーバーの性能は約 1.7 倍です。新規プロジェクトや、パフォーマンスが重要なアプリケーションに最適でしょう。

Deno は、セキュリティとモダンな開発体験を重視する方に向いています。TypeScript のネイティブサポート、Web 標準 API への準拠、セキュアなデフォルト設定が特徴です。パフォーマンスは Node.js と Bun の中間に位置します。

Node.js は、最も成熟したエコシステムと、圧倒的な数のパッケージが利用できる点が強みです。既存のプロジェクトや、特定のパッケージへの依存がある場合は、Node.js が最も安全な選択となります。

選択のポイント

どのランタイムを選ぶべきかは、プロジェクトの要件によって異なります。

以下の表は、ユースケース別の推奨ランタイムをまとめたものです。

#ユースケース推奨ランタイム理由
1新規 API サーバーBun高い HTTP 性能、開発体験の良さ
2セキュリティ重視Denoデフォルトでセキュア、権限管理が明確
3既存プロジェクトの移行Node.js → Bun互換性が高く、移行がスムーズ
4エンタープライズNode.js実績、エコシステム、サポートの充実
5バッチ処理Bun高速な I/O 性能
6CLI ツール開発Deno単一バイナリ配布が容易

パフォーマンスが最優先なら Bun、セキュリティとモダンさを重視するなら Deno、安定性と互換性を重視するなら Node.js が適しています。

今後の展望

JavaScript ランタイムの世界は急速に進化しています。

Bun はまだ 1.0 リリース直後であり、今後さらなる改善が期待されます。Deno も Node.js との互換性を高める取り組みを続けており、npm: プレフィックスによるパッケージ利用がより洗練されていくでしょう。

Node.js も、新しいランタイムからの刺激を受けて、パフォーマンス改善に取り組んでいます。最新の LTS バージョンでは、起動時間の短縮や HTTP パフォーマンスの向上が図られています。

どのランタイムを選ぶにしても、定期的にベンチマークを取り直し、最新の状況を把握することが重要です。本記事の測定方法を参考に、ぜひご自身の環境でも比較してみてください。

実際のプロジェクトでは、パフォーマンスだけでなく、チームの習熟度、エコシステムの成熟度、長期的な保守性なども考慮して、最適なランタイムを選択することをおすすめします。

関連リンク

各ランタイムの公式サイトと、ベンチマーク関連の情報をまとめました。