🥟

Bunが気になったのでJSランタイムを体系的に調べてみた

に公開

はじめに

最近、Bunの話題をよく目にするようになった。

2025年末にAnthropicがBunを買収したニュース。「依存パッケージのインストールが35倍速い」というベンチマーク。Xのタイムラインに流れてくるアップデート情報。気になって調べ始めたものの、「とにかく速い」以上の理解がなかなか進まなかった。

そもそもBunとNode.jsは何が違うのか。「JSランタイム」と「JSエンジン」はどう違うのか。Denoはどういう立ち位置なのか。最近よく聞く「エッジランタイム」とは何なのか。

一つ調べると次の疑問が出てくる。結局、JSランタイムという領域を体系的に理解しないと、個々の技術の位置づけがわからないと気づいた。

この記事では、Bunをきっかけに調べたJSランタイムの全体像を整理する。エンジンの仕組みからランタイムの設計思想、エッジランタイム、標準化の動きまで、一本の線でつながるように書いたつもりだ。

そもそもJSランタイムとは何か

JSエンジン ≠ JSランタイム

まず最初にはっきりさせておきたいのが、「JSエンジン」と「JSランタイム」は別物だということ。

JSエンジンは、JavaScriptのコードを受け取って実行する、文字通りの「エンジン」だ。V8(Google製)、JavaScriptCore(Apple製)、SpiderMonkey(Mozilla製)などがある。やることはシンプルで、「JSを読んで、実行する」。これだけ。

ただし、エンジン単体でできるのは純粋な計算だけだ。

// エンジンだけでできる
const result = [1, 2, 3].map(x => x * 2);

// エンジンだけではできない
fs.readFile('./data.txt');        // ファイル読み込み
fetch('https://api.example.com'); // ネットワーク通信
setTimeout(() => {}, 1000);       // タイマー

ファイルの読み書き、ネットワーク通信、タイマー——これらはすべてOSが管理するリソースであり、JSエンジンは直接OSと話す手段を持っていない。

ここで登場するのがJSランタイムだ。ランタイムは、JSエンジンに「OSとの橋渡し」を足した統合実行環境と考えるとわかりやすい。

ちなみに、ブラウザもJSランタイムの一種だ。ChromeはV8にDOM APIやWeb APIを載せた実行環境であり、Node.jsは同じV8にfs、net、httpなどのサーバー向けAPIを載せた実行環境。エンジンは同じでも、周りに何を付けるかで全く別のランタイムになる

「Node.jsではdocument.getElementByIdが使えない」「ブラウザではfs.readFileが使えない」——これはエンジンの違いではなく、ランタイムが提供するAPIの違いだ。

イベントループ — シングルスレッドなのに「待たない」仕組み

JSはシングルスレッドの言語だ。一度に一つの処理しか実行できない。

にもかかわらず、こんなコードは普通に動く。

console.log("開始");
setTimeout(() => console.log("2秒後"), 2000);
console.log("終了");
// → 開始 → 終了 → 2秒後

2秒待ってる間にメインスレッドがブロックされるなら、「終了」は2秒後に出るはずだ。でも実際は「終了」が先に出る。なぜか。

答えは、OSの力が必要な処理はOSに投げて、自分は先に進むから。

  1. JSエンジンがsetTimeoutを実行する
  2. ランタイムが「2秒タイマー」をOSに預ける
  3. JSエンジンは待たずに次の行(console.log("終了"))を実行する
  4. 2秒後、OSから「タイマー終わったよ」と通知が来る
  5. イベントループがその通知を拾い、コールバックを実行する

イベントループとは、この「OSからの完了通知を監視して、コールバックをメインスレッドに渡す」役割を担う仕組みのことだ。名前の通り、ループ(ぐるぐる回る)してイベント(完了通知)を拾い続ける。

ここで重要なのは、この仕組みはJSエンジンの仕事ではなく、ランタイムの仕事だということ。JSエンジンはコードを実行するだけ。「OSに投げる」「通知を拾う」はランタイムが担っている。

同期と非同期

ここまで理解すると、同期処理と非同期処理の違いも見えてくる。

// 同期 — 終わるまで次に進まない
const data = fs.readFileSync('./big.txt');
console.log("次の処理"); // ↑が終わるまで実行されない

// 非同期 — 待たずに先に進む
fs.readFile('./big.txt', (err, data) => {
  console.log("読み終わった");
});
console.log("次の処理"); // ← すぐ実行される

非同期 = 「待たない」仕組みだ。OSに処理を投げて、自分はさっさと次に進む。結果が返ってきたらイベントループが拾ってコールバックを実行する。

ではasync/awaitは何なのか。

async function getUser() {
  const res = await fetch('/api/user');
  const data = await res.json();
  return data;
}

getUser();
console.log("先に進むよ"); // ← 待たずに実行される

awaitは「この関数の中では結果が来るまで次の行に進まないで」という意味だ。fetchの結果がないと次の行でエラーになるから、関数内の実行順序は保証したい。しかし、関数の外の世界は止まらないgetUser()の呼び出し元は、awaitの完了を待たずに先に進む。

まとめると

JSランタイムの動き方は、こう整理できる。

  1. JSエンジンがコードを上から順に実行する
  2. OSの力が必要な処理(ファイル / ネットワーク / タイマー等)はOSに投げる
  3. 投げたら待たずに先へ進む(非同期)
  4. イベントループがOSからの完了通知を監視する
  5. 通知が来たらコールバックを実行する

この仕組みをJSエンジン単体ではなく、イベントループやシステムAPIも含めた「統合実行環境」として提供しているのが、JSランタイムだ。

JSエンジンの違い — V8 vs JavaScriptCore

ランタイムの心臓部であるJSエンジンについて、もう少し深掘りしてみる。

JITコンパイルという仕組み

CPUが理解できるのは機械語(マシンコード)だけだ。人間が書いたJSを実行するには、どこかで機械語に変換する必要がある。

変換の方法は大きく2つある。

  • インタプリタ方式: 1行ずつ翻訳しながら実行する。すぐ始められるが、繰り返し実行すると毎回翻訳するので遅い。
  • コンパイル方式: 先に全部機械語に翻訳してから実行する。実行は速いが、翻訳に時間がかかるので起動が遅い。

現代のJSエンジンは、この2つのいいとこ取りをしたJIT(Just-In-Time)コンパイルを採用している。

  1. まずインタプリタで実行を開始する(起動が速い)
  2. 何度も実行される部分(ホットスポット)を検知する
  3. そこだけを機械語にコンパイルする(次回からは速い)

外国語の本を読むとき、最初は辞書を引きながら読むけど、何度も出てくる単語は覚えてしまう——JITはそんなイメージだ。

V8とJavaScriptCore、最適化戦略の違い

どちらもJITを使うが、最適化の進め方が違う

V8(Node.js / Denoが採用)

Ignition(インタプリタ)
  → Sparkplug(軽いコンパイル)
    → Maglev(中くらいの最適化)
      → TurboFan(最大限の最適化)

慎重に段階を踏み、十分な実行データを集めてから最適化する。長時間動くほどピーク性能が上がる。元々はChrome向けに設計されたエンジンで、タブを長時間開き続ける使い方に最適化されている。

JavaScriptCore(Bunが採用)

LLInt(インタプリタ)
  → Baseline JIT(すぐコンパイル)
    → DFG JIT(中くらいの最適化)
      → FTL JIT(最大限の最適化)

同じ4段階だが、早い段階からコンパイルに入る。起動直後から速い。元々はSafari / iOS向けのエンジンで、モバイルのバッテリー効率やメモリ制約を考慮した設計になっている。

V8 JavaScriptCore
得意なこと 長時間実行のピーク性能 起動の速さ、メモリ効率
最適化の方針 じっくり分析してから最適化 早い段階からコンパイルして走り出す
設計の背景 Chrome(タブは長時間開く) Safari / iOS(省電力、省メモリ)

Bunの起動が5〜15msと速い理由の一つがここにある。JavaScriptCoreの「まず走り出す」設計思想が、CLIツールや短命なスクリプトの実行速度に直結しているのだ。

Node.js / Deno / Bun — 三者三様の思想

エンジンの違いがわかったところで、それぞれのランタイムが何を解決しようとして作られたのかを見ていく。スペック比較だけなら表で済むが、設計判断の背景を知ると「なぜこうなっているのか」が見えてくる。

Node.js — エコシステムの王

2009年、Ryan Dahlによって作られた。「ブラウザの外でJSを動かす」という当時の課題を解決し、サーバーサイドJSの時代を切り開いた。

Node.jsの最大の強みはnpmエコシステムだ。200万以上のパッケージが存在し、「やりたいことは大体誰かが作っている」状態にある。この規模のエコシステムは他のランタイムには真似できない。

一方で、15年以上の歴史がある分、設計上の負債も抱えている。

  • CommonJS / ESM問題: require()import、2つのモジュールシステムが共存している。ライブラリがどちらに対応しているかでハマることがあり、エコシステム全体の移行も完了していない
  • TypeScript対応が後追い: v22.6(2024年)でようやくネイティブTS実行に対応。それまではts-nodeやtsx等の外部ツールが必要だった
  • セキュリティがオープンすぎる: デフォルトでファイルもネットワークもフルアクセス可能。npmパッケージに悪意のあるコードが仕込まれた場合、ファイルシステムも環境変数も読み放題になる

特に3つ目のセキュリティ問題は、npmのサプライチェーン攻撃として現実に何度も起きている。この問題意識が、次のDenoの設計思想に直結する。

Deno — 「Node.jsをやり直す」

Denoの作者はRyan Dahl。Node.jsと同じ人だ。2018年のJSConf EUで「Node.jsについて後悔している10のこと」という講演を行い、その反省から新しいランタイムを一から作った。

セキュリティがデフォルト

Denoの最大の特徴は、許可制のセキュリティモデルだ。

# ネットワークアクセスには明示的な許可が必要
deno run --allow-net server.ts

# ファイルアクセスもディレクトリ単位で制御できる
deno run --allow-read=./data server.ts

npmパッケージが裏でネットワーク通信しようとしても、--allow-netを付けていなければブロックされる。Node.jsでは気づけない問題を、Denoなら構造的に防げる。

TypeScript first-class

最初からTSをそのまま実行できる。tsconfigもts-nodeも不要。

Web標準API準拠

ブラウザと同じfetch()Request/Responseがそのまま使える。Node.js独自のAPIではなく、Web標準に寄せる方針を取っている。

現実との折り合い

当初はURL importのみでnpmを排除する方針だったが、Deno 2以降はnpmパッケージをそのまま使えるようになった。理想と現実のバランスを取る方向に舵を切っている。

ただし、エコシステムの規模はNode.jsに遠く及ばず、採用企業もSlack Platform、Supabase Edge Functions等にまだ限られている。

Bun — 「全部入りで、とにかく速い」

2022年にJarred Sumner(当時22歳)が公開し、2025年末にAnthropicが買収した。

Bunの思想は明快だ。「開発者が使うツールを全部1つに入れて、全部速くする」

Node.jsの開発では、実行・パッケージ管理・バンドル・テスト・トランスパイルをそれぞれ別のツールで行う。

実行:           node
パッケージ管理:  npm / yarn / pnpm
バンドラ:       webpack / vite
テスト:         jest / vitest
トランスパイラ:  tsc / babel

Bunでは、これが全部bunコマンドひとつで済む。

bun init         # プロジェクト作成
bun add express  # パッケージ追加(npmの6〜35倍速い)
bun run index.ts # 実行(TSそのまま)
bun test         # テスト
bun build        # バンドル

全部入りのメリットは速度だけではない。ツール間の設定の不整合が起きない。Node.jsで「webpackとjestでTypeScript設定が微妙に違う」「ESM/CJSの解決がツールごとに異なる」といった地雷を踏んだことがある人なら、この価値がわかるはずだ。

一方で、トレードオフもある。

  • Node.js互換は95〜98%: 残りの数%で動かないパッケージがある。プロダクション導入時にはここが障壁になりうる
  • Bun自体のバグが全部に影響する: バンドラのバグでテストランナーも巻き添え、という可能性がある
  • セキュリティモデルはNode.jsと同じ: デフォルトでフルアクセス。Denoが解決した問題をBunは引き継いでいない
  • まだ若い: 情報量、安定性、エッジケースの対応でNode.jsには及ばない

三者の思想を整理する

ランタイム 思想 解決しようとしている課題
Node.js エコシステムの力で何でもできる JSをサーバーで使いたい
Deno 安全で正しいものを作り直す Node.jsの設計上の後悔
Bun 全部入りで速くする ツールの分散と遅さ

3つとも「JSランタイム」だが、解決しようとしている課題が違う。Node.jsは汎用性、Denoはセキュリティと正しさ、Bunは速度とDX(開発体験)。だから単純な優劣ではなく、何を重視するかで選ぶ話になる。

エッジランタイムという別のレイヤー

ここまでNode.js、Deno、Bunを見てきたが、これらはすべて「サーバーで動くランタイム」だ。エッジランタイムは少し毛色が違う。「コードをどこで実行するか」の話になる。

CDN — コンテンツを世界中に配る

まず前提として、CDN(Content Delivery Network)の話をしておく。

東京にサーバーを置いてWebサイトを運営しているとする。ブラジルのユーザーがアクセスすると、物理的な距離のせいでレスポンスに数百ミリ秒かかる。光の速度にも限界がある。

CDNはこの問題を、世界中にサーバーの拠点を置いて、ユーザーに最も近い場所からコンテンツを返すことで解決する。画像、CSS、JSファイルなどの静的ファイルを各拠点にキャッシュして配信するのが、元々のCDNの役割だ。

エッジランタイム — CDNの上でコードが動く

従来のCDNは「ファイルを配るだけ」だった。しかし最近は、CDNの各拠点でJavaScriptを実行できるようになった。これがエッジランタイムだ。

従来:
  ユーザー → CDN(静的ファイルを返すだけ)→ オリジンサーバー(処理はここ)

エッジランタイム:
  ユーザー → CDNエッジ(ここでJSが動く)→ 必要な場合だけオリジンへ

認証チェック、A/Bテスト、地域ごとのコンテンツ出し分け、APIレスポンスの加工——こうした軽い処理をユーザーに最も近い拠点で実行できる。Cloudflare Workers、Vercel Edge Functions、Deno Deployなどが代表的なプラットフォームだ。

サーバーレスとエッジの関係

ここで「サーバーレスと何が違うの?」という疑問が出てくるかもしれない。

従来のサーバーは、コンビニの24時間営業のようなものだ。客が来ようが来まいが、ずっと店を開けて待機していなければならない。深夜3時に誰も来なくても、電気代も人件費もかかる。

サーバーレスは、この「ずっと開けておく」をやめた。注文が来たときだけ店を開け、処理が終わったら閉める。使った分だけ課金。

エッジランタイムは、このサーバーレスの仕組みを世界中のCDN拠点に展開したものだ。

実行モデル 実行場所
従来のサーバー 常時起動 特定のリージョン EC2, VPS
サーバーレス リクエスト単位で起動 特定のリージョン AWS Lambda
エッジ リクエスト単位で起動 世界中のCDN拠点 Cloudflare Workers

サーバーレスが「どう動くか(関数単位で実行)」の話なら、エッジは「どこで動くか(ユーザーに近い場所で実行)」の話だ。エッジランタイムは両方の特徴を併せ持っている。

V8 isolate — 軽さの仕組み

エッジランタイムが世界300拠点で動くためには、とにかく軽くなければならない。全拠点にフルのNode.jsプロセスを立てるのは現実的ではない。

ここで使われるのがV8 isolateという仕組みだ。

Node.jsでは、1つのアプリケーションに1つのOSプロセスを割り当てる。Cloudflare Workersでは、1つのプロセスの中にV8エンジンレベルで隔離された小部屋(isolate)をたくさん作り、それぞれの小部屋で別々のアプリケーションを動かす。

Node.js(1アプリ = 1プロセス):

Cloudflare Workers(1プロセスの中に複数のisolate):

テナントごとに1棟ビルを建てるか、1つのビルの中をパーティションで区切るかの違いだ。isolateは起動が5ms以下、メモリはNode.jsプロセスの約1/10。その代わり、ファイルシステムへのアクセスや長時間実行はできないという制約がある。

エッジランタイムの使いどころ

エッジは万能ではない。軽い処理をユーザーの近くで高速に捌くのが得意な一方、重い計算や長時間の処理には向かない。

エッジが得意なこと エッジに向かないこと
認証・認可チェック 重い計算処理
リクエストのルーティング 大量のDBクエリ
レスポンスの加工・キャッシュ 長時間のバッチ処理
A/Bテスト、地域出し分け 大きなファイルの生成

実務では「エッジかサーバーか」の二択ではなく、エッジで捌ける処理はエッジで、残りはオリジンサーバーでという使い分けが一般的だ。

WinterTC — ランタイム乱立時代の標準化

同じJSなのに動かない問題

ここまでNode.js、Deno、Bun、Cloudflare Workersと見てきた。ここで一つ問題がある。同じJavaScriptなのに、ランタイムごとにAPIが微妙に違うのだ。

例えばHTTPリクエストを受け取る書き方を比べてみる。

// Node.js
const http = require('http');
http.createServer((req, res) => {
  res.end('Hello');
});

// Deno / Bun / Cloudflare Workers
export default {
  fetch(request: Request): Response {
    return new Response('Hello');
  }
};

Node.jsは独自のhttpモジュール。Deno、Bun、WorkersはWeb標準のRequest/Responseを使っている。

ライブラリの作者からすると、これは厄介な話だ。「Node.jsでは動くけどDenoでは動かない」「Cloudflare Workersでは動くけどBunでは動かない」——同じJavaScriptのはずなのに、こういうことが起きる。

WinterTCとは何か

この問題を解決するために生まれたのがWinterTC(旧WinterCG)だ。2025年にEcma International(JavaScriptの標準化団体TC39と同じ組織)の中にTC55として正式に設置された。

参加しているのはCloudflare、Deno、Vercel、Node.js、Shopify、Bloombergなど。ランタイムを作っている側が集まり、「サーバーサイドJSランタイムが共通で実装すべきAPIの仕様」を策定している。

やっていることはシンプルで、ブラウザにあるWeb APIを、サーバーサイドのランタイムでも同じように使えるようにすることだ。fetch()Request/ResponseURLTextEncodercrypto.subtle——こうしたブラウザで動くAPIを「サーバーサイドでもこの通りに実装してください」と仕様にまとめている。2025年12月に「Minimum Common Web API」として最初の仕様が公開された。

なぜ「ブラウザが基準」なのか

WinterTCの面白い設計判断は、新しいAPIを発明するのではなく、すでにブラウザで使われているAPIをベースラインにするという点だ。

理由は明快で、ブラウザのAPIはすでに何十億人が使っていて十分に検証されている。フロントエンドエンジニアが新しいAPIを覚え直す必要もない。「ブラウザで動くコードがサーバーでもそのまま動く」——WinterTCはその世界を目指している。

なぜこれが重要なのか

WinterTCが実現しようとしているのは、サーバーサイドJSにおけるWrite once, run anywhereだ。

WinterTC準拠のAPIだけで書かれたコードは、Node.jsでもDenoでもBunでもCloudflare Workersでも動く。特定のランタイムに依存しないコードが書けるようになれば、ベンダーロックインのリスクも減る。「Cloudflare Workersで動かしてたけどDeno Deployに移行したい」というときに、コードの書き換えが不要になる。

ランタイムが乱立する今だからこそ、この標準化の動きは重要性を増している。

結局どう選ぶのか — 実務的な判断軸

ここまでの内容を踏まえて、実際にランタイムを選ぶときの判断軸を整理してみる。

「速い=正義」ではない

Bunの起動は5〜15ms、HTTPスループットはNode.jsの約4倍。数字だけ見ればBun一択に見える。しかし、技術選定においてベンチマークは判断材料の一つに過ぎない。

実際に考えるべき軸は複数ある。

安定性とエコシステム

Node.jsの200万パッケージという規模は、「やりたいことの大半にすでに解決策がある」ことを意味する。Bunの互換性が95〜98%ということは、残りの数%で動かないパッケージに当たる可能性があるということだ。プロダクションで突然動かないパッケージに遭遇するリスクをどう見るかは、プロジェクトの性質による。

チームが扱えるか

10人のチーム全員がNode.js経験者なのに「Bunの方が速いから」で移行すると、トラブル発生時に誰も対処法を知らないという状況が起きうる。情報量の差も大きい。Node.jsで困ったらStack Overflowに大量の事例があるが、Bunではまだそうはいかない。

セキュリティ要件

npmサプライチェーン攻撃が現実のリスクとして存在する以上、Denoのパーミッションモデルは合理的な選択肢だ。外部パッケージを多用するプロジェクトや、セキュリティ監査が厳しい環境では、Denoの「デフォルトで全ブロック」は大きな強みになる。

ユースケース別の選び方

ユースケース 向いてるランタイム 理由
企業のプロダクション Node.js 安定性、エコシステム、人材の採用しやすさ
個人開発・新規プロジェクト Bun 起動が速い、設定不要、全部入り
セキュリティ重視のシステム Deno パーミッションモデルによる構造的な防御
認証やルーティング等の軽い処理 エッジランタイム ユーザーに近い場所で低レイテンシに処理
CLIツール Bun 起動速度がUXに直結する

もちろん、これは絶対的な基準ではない。「企業のプロダクションでもBunを使う」という判断は十分ありえるし、実際にAnthropicはClaude Codeの基盤にBunを採用している。大事なのは、ベンチマークだけでなくプロジェクトの文脈を含めて判断することだ。

「全部Node.js」も正解になりうる

一つ強調しておきたいのは、Node.jsを選ぶことは保守的な判断ではないということだ。

Node.jsもv22.6以降でTypeScriptネイティブ実行に対応し、Web標準APIの採用を進めている。ESMへの移行も着実に進んでいる。他のランタイムが切り拓いた道を、Node.jsが巨大なエコシステムを保ちながら追いかけている構図だ。

現時点で「全部Node.jsでいく」と決めたとしても、Web標準APIを意識して書いていれば、WinterTCの標準化が進んだ将来に他のランタイムへ移行しやすくなる。今の選択が将来の選択肢を狭めない——それが標準化の恩恵だ。

まとめ

Bunが気になったことをきっかけに、JSランタイムを体系的に調べてみた。見えてきたのは、ランタイムの違いは単なるスペック差ではなく、思想の違いだということだ。

  • Node.jsは、巨大なエコシステムという武器で「何でもできる」世界を作った
  • Denoは、Node.jsの設計上の後悔から「安全で正しいもの」を作り直した
  • Bunは、ツールの分散と遅さに対して「全部入りで速くする」と答えた
  • エッジランタイムは、「コードをユーザーの近くで動かす」という新しいレイヤーを切り拓いた
  • WinterTCは、乱立するランタイム間の「共通言語」を作ろうとしている

どれが一番優れているかではなく、何を重視するかで最適解が変わる。そして、WinterTCによる標準化が進むことで、その選択はより柔軟になっていく。

JSランタイムは今、面白い時代にいる。

Discussion