Bun本番運用:うまくいくこと、いかないこと、驚いたこと
ランタイム、パッケージマネージャー、バンドラー、テストランナーとしてのBun。実ベンチマーク、Node.js互換性ギャップ、移行パターン、プロダクションでBunを使っている場面。
数年おきにJavaScriptエコシステムに新しいランタイムが登場し、議論は予測可能な軌道をたどる。ハイプ。ベンチマーク。「Xは死んだ。」現実チェック。新しいツールが本当に輝くユースケースへの収束。
Bunは今まさにその軌道の途中にいる。そして、ほとんどの挑戦者と違い、定着しつつある。「速い」からではなく(実際に速いことが多いが)、本当に異なる問題を解決しているからだ:JavaScriptツールチェーンには可動部品が多すぎるが、Bunはそれを1つにまとめる。
私はBunを1年以上、さまざまな場面で使ってきた。一部は本番環境で。一部は、まさか置き換えることはないと思っていたツールの代替として。この記事は、何がうまくいくか、何がだめか、そしてギャップがまだ重要な場面についての正直な報告だ。
Bunの正体#
まず払拭すべき誤解:Bunは「より速いNode.js」ではない。そのフレーミングは過小評価だ。
Bunは1つのバイナリに4つのツールを内蔵している:
- JavaScript/TypeScriptランタイム — Node.jsやDenoのようにコードを実行する
- パッケージマネージャー — npm、yarn、pnpmの代替
- バンドラー — 特定のユースケースでesbuild、webpack、Rollupの代替
- テストランナー — ほとんどのテストスイートでJestやVitestの代替
Node.jsとの主要なアーキテクチャの違いはエンジンだ。Node.jsはV8(Chromeのエンジン)を使用する。BunはJavaScriptCore(Safariのエンジン)を使用する。どちらも成熟した本番グレードのエンジンだが、異なるトレードオフがある。JavaScriptCoreは起動時間が速くメモリオーバーヘッドが低い傾向がある。V8は長時間実行される計算でピークスループットが良い傾向がある。実際には、ほとんどのワークロードでこれらの違いは思ったより小さい。
もう1つの主要な差別化要因:BunはZigで書かれている。Zigはシステムプログラミング言語で、Cとほぼ同じレベルにあるが、より良いメモリ安全性の保証を持つ。これがBunがパフォーマンスで攻めることができる理由だ — ZigはCが提供するような低レベルの制御を、Cの落とし穴の密度なしに提供する。
# Bunのバージョンを確認
bun --version
# TypeScriptファイルを直接実行 — tsconfigもコンパイルステップも不要
bun run server.ts
# パッケージをインストール
bun install
# テストを実行
bun test
# 本番用にバンドル
bun build ./src/index.ts --outdir ./distこれは node + npm + esbuild + vitest の仕事を1つのバイナリでこなす。好き嫌いはあるだろうが、複雑さの削減としては魅力的だ。
速度の主張 — 正直なベンチマーク#
率直に言おう:Bunのマーケティングベンチマークはチェリーピックされている。詐欺ではない — チェリーピックだ。Bunが最もよく性能を発揮するシナリオを示しており、それはマーケティング資料として予想通りだ。問題は、人々がそれらのベンチマークから推定して、Bunがすべてにおいて「25倍速い」と主張することで、それは絶対に事実ではない。
Bunが本当に、意味のある速さを持つ場面はここだ:
起動時間#
これはBunの最大の本当の優位性であり、比較にならない。
# 起動時間の計測 — それぞれ100回実行
hyperfine --warmup 5 'node -e "console.log(1)"' 'bun -e "console.log(1)"'
# 典型的な結果:
# node: 〜40ms
# bun: 〜6msおおよそ6〜7倍の起動時間の差がある。スクリプト、CLIツール、コールドスタートが重要なサーバーレスファンクションでは、これは大きい。一度起動して数週間動き続ける長寿命のサーバープロセスでは、無関係だ。
パッケージインストール#
これもBunが競合を圧倒する分野だ。
# クリーンインストールのベンチマーク — まずnode_modulesとロックファイルを削除
rm -rf node_modules bun.lockb package-lock.json
# npmの時間計測
time npm install
# Real: 〜18.4秒(典型的な中規模プロジェクト)
# bunの時間計測
time bun install
# Real: 〜2.1秒8〜9倍の差があり、一貫している。主な理由は:
- バイナリロックファイル —
bun.lockbはJSONではなくバイナリ形式。読み書きが速い。 - グローバルキャッシュ — Bunはグローバルモジュールキャッシュを維持し、プロジェクト間の再インストールでダウンロード済みパッケージを共有する。
- ZigのI/O — パッケージマネージャー自体がJavaScriptではなくZigで書かれている。ファイルI/O操作がより低レベルに近い。
- シンボリックリンク戦略 — Bunはファイルをコピーする代わりに、グローバルキャッシュからハードリンクを使用する。
HTTPサーバースループット#
Bunの組み込みHTTPサーバーは速いが、比較にはコンテキストが必要だ。
# bombardierを使った簡易ベンチマーク
# シンプルな「Hello World」レスポンスのテスト
# Bunサーバー
bombardier -c 100 -d 10s http://localhost:3000
# リクエスト/秒: 〜105,000
# Node.js(ネイティブhttpモジュール)
bombardier -c 100 -d 10s http://localhost:3001
# リクエスト/秒: 〜48,000
# Node.js(Express)
bombardier -c 100 -d 10s http://localhost:3002
# リクエスト/秒: 〜15,000Bun対素のNode.js:些細なレスポンスでおおよそ2倍。Bun対Express:おおよそ7倍だが、Expressはミドルウェアのオーバーヘッドがあるため不公平だ。実際のロジック — データベースクエリ、認証、実データのJSONシリアライゼーション — を追加した途端、差は劇的に縮まる。
差がほぼないところ#
CPU集約型の計算:
// fibonacci.ts — これはエンジン依存であり、ランタイム依存ではない
function fib(n: number): number {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
const start = performance.now();
console.log(fib(42));
console.log(`${(performance.now() - start).toFixed(0)}ms`);bun run fibonacci.ts # 〜1650ms
node fibonacci.ts # 〜1580msNode.js(V8)が実際にここではわずかに勝つ。V8のJITコンパイラはホットループでより積極的だ。CPU集約的な作業では、エンジンの違いは拮抗している — 時にV8が勝ち、時にJSCが勝ち、差はノイズの範囲内だ。
自分でベンチマークを実行する方法#
私のものを含め、誰のベンチマークも信用するな。あなたの具体的なワークロードにとって重要なことを計測する方法:
# 適切なベンチマーク用にhyperfineをインストール
brew install hyperfine # macOS
# または: cargo install hyperfine
# 実際のアプリの起動+実行をベンチマーク
hyperfine --warmup 3 \
'node dist/server.js' \
'bun src/server.ts' \
--prepare 'sleep 0.1'
# HTTPサーバーにはbombardierまたはwrkを使用
# 重要:「Hello World」ではなく現実的なペイロードでテスト
bombardier -c 50 -d 30s -l http://localhost:3000/api/users
# メモリ比較
/usr/bin/time -v node server.js # Linux
/usr/bin/time -l bun server.ts # macOS経験則:ボトルネックがI/O(ファイルシステム、ネットワーク、データベース)なら、Bunの優位性は控えめ。ボトルネックが起動時間やツールチェーン速度なら、Bunの大勝利。ボトルネックが生の計算なら、五分五分だ。
パッケージマネージャーとしてのBun#
ここは私が完全に切り替えた領域だ。本番環境でNode.jsを実行しているプロジェクトでも、ローカル開発とCIでは bun install を使っている。単純に速く、互換性も優秀だ。
基本操作#
# package.jsonからすべての依存関係をインストール
bun install
# 依存関係を追加
bun add express
# 開発依存関係を追加
bun add -d vitest
# 依存関係を削除
bun remove express
# 依存関係を更新
bun update express
# 特定バージョンをインストール
bun add express@4.18.2npmやyarnを使ったことがあれば、完全に馴染みのある操作だ。フラグが少し異なるが(--save-dev の代わりに -d)、メンタルモデルは同じだ。
ロックファイルの状況#
Bunは bun.lockb というバイナリロックファイルを使用する。これはその最強の武器であると同時に、最大の摩擦点でもある。
良い点:読み書きが劇的に速い。バイナリ形式のため、Bunはロックファイルをマイクロ秒で解析できる。npmが package-lock.json の解析に費やす数百ミリ秒ではない。
悪い点:diffでレビューできない。チームで誰かが依存関係を更新した場合、PRでロックファイルのdiffを見て何が変わったか確認できない。これは速度推進派が認めたがるより重要だ。
# ロックファイルを人間が読める形式にダンプ
bun bun.lockb > lockfile-dump.txt
# または組み込みのテキスト出力を使用
bun install --yarn
# bun.lockbと並行してyarn.lockを生成する私のアプローチ:bun.lockb をリポジトリにコミットし、読めるフォールバックとして yarn.lock または package-lock.json も生成する。ベルトとサスペンダーの両方だ。
ワークスペースサポート#
Bunはnpm/yarnスタイルのワークスペースをサポートしている:
{
"name": "my-monorepo",
"workspaces": [
"packages/*",
"apps/*"
]
}# すべてのワークスペースの依存関係をインストール
bun install
# 特定のワークスペースでスクリプトを実行
bun run --filter packages/shared build
# 特定のワークスペースに依存関係を追加
bun add react --filter apps/webワークスペースサポートは堅牢で、大幅に改善されている。pnpmと比較した主なギャップは、Bunのワークスペース依存関係解決がそれほど厳格でないこと — pnpmの厳格さはモノレポの機能だ。ファントム依存関係を検出するからだ。
既存プロジェクトとの互換性#
ほぼすべての既存Node.jsプロジェクトに bun install をそのまま導入できる。package.json を読み、レジストリ設定用の .npmrc を尊重し、peerDependencies を正しく処理する。移行は通常:
# ステップ1:既存のロックファイルとnode_modulesを削除
rm -rf node_modules package-lock.json yarn.lock pnpm-lock.yaml
# ステップ2:Bunでインストール
bun install
# ステップ3:アプリがまだ動作するか確認
bun run dev
# または: node dist/server.js(Bunパッケージマネージャー、Nodeランタイム)十数個のプロジェクトでこれを行い、パッケージマネージャー自体での問題はゼロだった。唯一の落とし穴は、CIパイプラインが package-lock.json を明示的に探している場合 — bun.lockb を処理するように更新が必要だ。
Node.js互換性#
このセクションは最も慎重に書く必要がある。状況が毎月変わるからだ。2026年初頭時点での正直な状況を紹介する。
動作するもの#
npmパッケージの大多数が変更なしで動作する。BunはほとんどのNode.js組み込みモジュールを実装している:
// これらはすべてBunで期待通りに動作する
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { Buffer } from "node:buffer";
import { EventEmitter } from "node:events";
import { Readable, Writable } from "node:stream";
import http from "node:http";
import https from "node:https";
import { URL, URLSearchParams } from "node:url";
import os from "node:os";
import child_process from "node:child_process";CommonJSとESMの両方が動作する。require() と import が共存できる。TypeScriptはコンパイルステップなしで実行される — Bunはパース時に型を除去する。
動作するフレームワーク:
- Express — ミドルウェアエコシステムを含めて動作
- Fastify — 動作
- Hono — 動作(Bunとの相性が特に良い)
- Next.js — 注意事項あり(後述)
- Prisma — 動作
- Drizzle ORM — 動作
- Socket.io — 動作
動作しない(または問題がある)もの#
ギャップはいくつかのカテゴリに分かれる傾向がある:
ネイティブアドオン(node-gyp):パッケージがnode-gypでコンパイルされたC++アドオンを使用している場合、Bunで動作しない可能性がある。Bunは独自のFFIシステムを持ち多くのネイティブモジュールをサポートしているが、カバレッジは100%ではない。例えば、bcrypt(ネイティブ版)には問題があった — 代わりに bcryptjs を使用する。
# パッケージがネイティブアドオンを使用しているか確認
ls node_modules/your-package/binding.gyp # これが存在すればネイティブ特定のNode.js内部API:一部のパッケージは process.binding() のようなNode.js内部やV8固有のAPIに手を伸ばす。BunはJavaScriptCoreで動作するため、これらは動作しない。
// これはBunで動作しない — V8固有
const v8 = require("v8");
v8.serialize({ data: "test" });
// これは動作する — Bunの同等機能やクロスランタイムのアプローチを使用
const encoded = new TextEncoder().encode(JSON.stringify({ data: "test" }));ワーカースレッド:BunはWeb Workersと node:worker_threads をサポートしているが、エッジケースがある。特に SharedArrayBuffer と Atomics に関する高度な使用パターンは動作が異なる場合がある。
vmモジュール:node:vm は部分的にサポートされている。コードや依存関係が vm.createContext() を多用している場合(一部のテンプレートエンジンが該当)、徹底的にテストすること。
互換性トラッカー#
Bunは公式の互換性トラッカーを維持している。プロジェクトでBunにコミットする前にチェックすること:
# プロジェクトでBunの組み込み互換性チェックを実行
bun --bun node_modules/.bin/your-tool
# --bunフラグはnode_modulesスクリプトにもBunのランタイムを強制する私の推奨:互換性を仮定しないこと。決定する前にBunでテストスイートを実行する。5分で済み、何時間ものデバッグを節約できる。
# クイック互換性チェック — Bunで完全なテストスイートを実行
bun test # bunテストランナーを使用する場合
# または
bun run vitest # vitestを使用する場合Bunの組み込みAPI#
ここがBunの面白いところだ。Node.jsのAPIを再実装するだけでなく、よりシンプルで高速に設計された独自のAPIを提供している。
Bun.serve() — 組み込みHTTPサーバー#
最もよく使うAPIだ。クリーンで、速く、WebSocketサポートが組み込まれている。
const server = Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/") {
return new Response("Hello from Bun!", {
headers: { "Content-Type": "text/plain" },
});
}
if (url.pathname === "/api/users") {
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
return Response.json(users);
}
return new Response("Not Found", { status: 404 });
},
});
console.log(`サーバー起動中: http://localhost:${server.port}`);いくつか注目すべき点:
- Web標準のRequest/Response — 独自APIなし。
fetchハンドラは標準のRequestを受け取り、標準のResponseを返す。Cloudflare Workerを書いたことがあれば、まったく同じ感覚だ。 Response.json()— 組み込みのJSONレスポンスヘルパー。- インポート不要 —
Bun.serveはグローバル。require("http")は不要。
ルーティング、JSONボディ解析、エラーハンドリングを含むより現実的な例:
import { Database } from "bun:sqlite";
const db = new Database("app.db");
db.run(`
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
)
`);
const server = Bun.serve({
port: process.env.PORT || 3000,
async fetch(req) {
const url = new URL(req.url);
const method = req.method;
try {
// GET /api/todos
if (url.pathname === "/api/todos" && method === "GET") {
const todos = db.query("SELECT * FROM todos ORDER BY created_at DESC").all();
return Response.json(todos);
}
// POST /api/todos
if (url.pathname === "/api/todos" && method === "POST") {
const body = await req.json();
if (!body.title || typeof body.title !== "string") {
return Response.json({ error: "タイトルは必須です" }, { status: 400 });
}
const stmt = db.prepare("INSERT INTO todos (title) VALUES (?) RETURNING *");
const todo = stmt.get(body.title);
return Response.json(todo, { status: 201 });
}
// DELETE /api/todos/:id
const deleteMatch = url.pathname.match(/^\/api\/todos\/(\d+)$/);
if (deleteMatch && method === "DELETE") {
const id = parseInt(deleteMatch[1], 10);
db.run("DELETE FROM todos WHERE id = ?", [id]);
return new Response(null, { status: 204 });
}
return Response.json({ error: "見つかりません" }, { status: 404 });
} catch (error) {
console.error("リクエストエラー:", error);
return Response.json({ error: "内部サーバーエラー" }, { status: 500 });
}
},
});
console.log(`サーバーがポート ${server.port} で起動中`);SQLiteを使った完全なCRUD APIが約50行。Express不要、ORM不要、ミドルウェアチェーン不要。小さなAPIや社内ツールでは、これが今の私のお気に入りのセットアップだ。
Bun.file() と Bun.write() — ファイルI/O#
BunのファイルAPIは fs.readFile() と比べて爽快なほどシンプルだ:
// ファイルの読み取り
const file = Bun.file("./config.json");
const text = await file.text(); // 文字列として読み取り
const json = await file.json(); // JSONとして直接パース
const bytes = await file.arrayBuffer(); // ArrayBufferとして読み取り
const stream = file.stream(); // ReadableStreamとして読み取り
// ファイルメタデータ
console.log(file.size); // バイト単位のサイズ
console.log(file.type); // MIMEタイプ(例:"application/json")
// ファイルの書き込み
await Bun.write("./output.txt", "Hello, World!");
await Bun.write("./data.json", JSON.stringify({ key: "value" }));
await Bun.write("./copy.png", Bun.file("./original.png"));
// Responseボディをファイルに書き込み
const response = await fetch("https://example.com/data.json");
await Bun.write("./downloaded.json", response);Bun.file() APIは遅延評価だ — .text()、.json() などを呼ぶまでファイルは読まれない。つまり、実際にデータが必要になるまでI/Oコストなしに Bun.file() の参照を渡し回せる。
組み込みWebSocketサポート#
Bun.serve() ではWebSocketがファーストクラスだ:
const server = Bun.serve({
port: 3000,
fetch(req, server) {
const url = new URL(req.url);
if (url.pathname === "/ws") {
const upgraded = server.upgrade(req, {
data: {
userId: url.searchParams.get("userId"),
joinedAt: Date.now(),
},
});
if (!upgraded) {
return new Response("WebSocketアップグレードに失敗", { status: 400 });
}
return undefined;
}
return new Response("WebSocket接続には /ws を使用してください");
},
websocket: {
open(ws) {
console.log(`クライアント接続: ${ws.data.userId}`);
ws.subscribe("chat");
},
message(ws, message) {
// すべてのサブスクライバーにブロードキャスト
server.publish("chat", `${ws.data.userId}: ${message}`);
},
close(ws) {
console.log(`クライアント切断: ${ws.data.userId}`);
ws.unsubscribe("chat");
},
},
});server.publish() と ws.subscribe() パターンは組み込みのpub/subだ。Redis不要、別途WebSocketライブラリ不要。シンプルなリアルタイム機能には、信じられないほど便利だ。
bun:sqlite による組み込みSQLite#
これが一番驚いた。BunはSQLiteをランタイムに直接組み込んでいる:
import { Database } from "bun:sqlite";
// データベースを開くまたは作成
const db = new Database("myapp.db");
// 並行読み取り性能向上のためWALモード
db.exec("PRAGMA journal_mode = WAL");
// テーブルの作成
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
)
`);
// プリペアドステートメント(再利用可能、繰り返しクエリで高速)
const insertUser = db.prepare(
"INSERT INTO users (email, name) VALUES ($email, $name) RETURNING *"
);
const findByEmail = db.prepare(
"SELECT * FROM users WHERE email = $email"
);
// 使用方法
const user = insertUser.get({
$email: "alice@example.com",
$name: "Alice",
});
console.log(user); // { id: 1, email: "alice@example.com", name: "Alice", ... }
// トランザクション
const insertMany = db.transaction((users: { email: string; name: string }[]) => {
for (const user of users) {
insertUser.run({ $email: user.email, $name: user.name });
}
return users.length;
});
const count = insertMany([
{ email: "bob@example.com", name: "Bob" },
{ email: "carol@example.com", name: "Carol" },
]);
console.log(`${count}人のユーザーを挿入`);これはCライブラリのパフォーマンスを持つ同期SQLiteだ(実際にそうだから — Bunはlibsqlite3を直接埋め込んでいる)。CLIツール、ローカルファーストアプリ、小さなサービスでは、組み込みSQLiteはデータレイヤーの外部依存関係がゼロということを意味する。
Bunテストランナー#
bun test はほとんどの場合、Jestのドロップイン置換だ。同じ describe/it/expect APIを使用し、ほとんどのJestマッチャーをサポートしている。
基本的な使い方#
// math.test.ts
import { describe, it, expect } from "bun:test";
describe("数学ユーティリティ", () => {
it("数値を正しく加算する", () => {
expect(1 + 2).toBe(3);
});
it("浮動小数点を処理する", () => {
expect(0.1 + 0.2).toBeCloseTo(0.3);
});
});# すべてのテストを実行
bun test
# 特定のファイルを実行
bun test math.test.ts
# パターンに一致するテストを実行
bun test --test-name-pattern "数値を正しく加算"
# ウォッチモード
bun test --watch
# カバレッジ
bun test --coverageモック#
BunはJest互換のモックをサポートしている:
import { describe, it, expect, mock, spyOn } from "bun:test";
import { fetchUsers } from "./api";
// モジュールをモック
mock.module("./database", () => ({
query: mock(() => [{ id: 1, name: "Alice" }]),
}));
describe("fetchUsers", () => {
it("データベースからユーザーを返す", async () => {
const users = await fetchUsers();
expect(users).toHaveLength(1);
expect(users[0].name).toBe("Alice");
});
});
// オブジェクトメソッドをスパイ
describe("console", () => {
it("console.logの呼び出しを追跡する", () => {
const logSpy = spyOn(console, "log");
console.log("テストメッセージ");
expect(logSpy).toHaveBeenCalledWith("テストメッセージ");
logSpy.mockRestore();
});
});Bun Test vs. Vitest — 正直な比較#
このプロジェクト(そして私のほとんどのプロジェクト)ではVitestを使っている。完全に切り替えていない理由はこうだ:
bun testが勝つところ:
- 起動速度。
bun testはVitestが設定の読み込みを完了する前にテスト実行を開始する。 - ゼロコンフィグ。基本的なセットアップに
vitest.config.tsが不要。 - 組み込みTypeScript。変換ステップなし。
Vitestがまだ勝つところ:
- エコシステム:Vitestはプラグインが多く、IDE統合が良く、コミュニティが大きい。
- 設定:Vitestの設定システムはより柔軟。カスタムレポーター、複雑なセットアップファイル、複数のテスト環境。
- ブラウザモード:Vitestは実際のブラウザでテストを実行できる。Bunはできない。
- 互換性:一部のテストライブラリ(Testing Library、MSW)はVitest/Jestでより徹底的にテストされている。
- スナップショットテスト:両方サポートしているが、Vitestの実装はより成熟しており、diff出力が良い。
シンプルなテストニーズの新プロジェクトなら、bun test を使う。Testing Library、MSW、複雑なモックを持つ既存プロジェクトなら、Vitestを続ける。
Bunバンドラー#
bun build は高速なJavaScript/TypeScriptバンドラーだ。webpackの置換ではない — esbuildのカテゴリに近い:高速で、意見があり、一般的なケースに焦点を当てている。
基本的なバンドリング#
# 単一のエントリーポイントをバンドル
bun build ./src/index.ts --outdir ./dist
# 異なるターゲット向けにバンドル
bun build ./src/index.ts --outdir ./dist --target browser
bun build ./src/index.ts --outdir ./dist --target bun
bun build ./src/index.ts --outdir ./dist --target node
# ミニファイ
bun build ./src/index.ts --outdir ./dist --minify
# ソースマップを生成
bun build ./src/index.ts --outdir ./dist --sourcemap externalプログラマティックAPI#
const result = await Bun.build({
entrypoints: ["./src/index.ts", "./src/worker.ts"],
outdir: "./dist",
target: "browser",
minify: {
whitespace: true,
identifiers: true,
syntax: true,
},
splitting: true, // コード分割
sourcemap: "external",
external: ["react", "react-dom"], // これらはバンドルしない
naming: "[dir]/[name]-[hash].[ext]",
define: {
"process.env.NODE_ENV": JSON.stringify("production"),
},
});
if (!result.success) {
console.error("ビルド失敗:");
for (const log of result.logs) {
console.error(log);
}
process.exit(1);
}
for (const output of result.outputs) {
console.log(`${output.path} — ${output.size} バイト`);
}ツリーシェイキング#
BunはESMのツリーシェイキングをサポートしている:
// utils.ts
export function used() {
return "バンドルに含まれる";
}
export function unused() {
return "ツリーシェイキングで除去される";
}
// index.ts
import { used } from "./utils";
console.log(used());bun build ./src/index.ts --outdir ./dist --minify
# `unused` 関数は出力に現れないBun Buildの限界#
- CSSバンドリングなし — CSSには別途ツールが必要(PostCSS、Lightning CSS、Tailwind CLI)。
- HTML生成なし — JavaScript/TypeScriptをバンドルするが、完全なWebアプリではない。
- プラグインエコシステム — esbuildの方がプラグインエコシステムがはるかに大きい。BunのプラグインAPIは互換だが、コミュニティは小さい。
- 高度なコード分割 — WebpackとRollupの方がより洗練されたチャンク戦略を提供する。
ライブラリやシンプルなWebアプリのJSバンドルの構築には、bun build は優秀だ。CSSモジュール、画像最適化、カスタムチャンク戦略を含む複雑なアプリビルドには、フルバンドラーが必要だろう。
Bunマクロ#
本当にユニークな機能:マクロによるコンパイル時のコード実行。
// build-info.ts — このファイルはランタイムではなくビルド時に実行される
export function getBuildInfo() {
return {
builtAt: new Date().toISOString(),
gitSha: require("child_process")
.execSync("git rev-parse --short HEAD")
.toString()
.trim(),
nodeVersion: process.version,
};
}// app.ts
import { getBuildInfo } from "./build-info" with { type: "macro" };
// getBuildInfo()はバンドル時に実行される
// 結果は静的な値としてインライン化される
const info = getBuildInfo();
console.log(`ビルド日時: ${info.builtAt}, コミット: ${info.gitSha}`);バンドル後、getBuildInfo() はリテラルオブジェクトに置換される — ランタイムでの関数呼び出しも child_process のインポートもない。コードはビルド中に実行され、結果がインライン化された。ビルドメタデータ、フィーチャーフラグ、環境固有の設定の埋め込みに強力だ。
BunとNext.js#
最もよく聞かれる質問なので、具体的に説明しよう。
現在動作するもの#
Next.jsのパッケージマネージャーとしてのBun — 完璧に動作:
# Bunで依存関係をインストールし、Node.jsでNext.jsを実行
bun install
bun run dev # デフォルトではNode.js経由で「dev」スクリプトを実行
bun run build
bun run startすべてのNext.jsプロジェクトでこれを行っている。bun run <script> コマンドは package.json の scripts セクションを読み取り実行する。デフォルトでは、実際の実行にはシステムのNode.jsを使用する。ランタイムを変更せずに、Bunの高速パッケージインストールの恩恵が得られる。
Next.js開発用のBunランタイム:
# Next.jsをBunのランタイムで強制実行
bun --bun run devほとんどの場合、開発で動作する。--bun フラグはNode.jsに委譲する代わりにBun独自のランタイムを使うよう指示する。ホットモジュールリプレースメントが動作する。APIルートが動作する。サーバーコンポーネントが動作する。
まだ実験的なもの#
Next.js本番ビルド用のBunランタイム:
# Bunランタイムでビルド
bun --bun run build
# Bunランタイムで本番サーバーを起動
bun --bun run start多くのプロジェクトで動作するが、エッジケースに遭遇した:
- 一部のミドルウェアの動作が異なる — Node.js固有のAPIに依存するNext.jsミドルウェアを使用している場合、互換性の問題に当たる可能性がある。
- 画像最適化 — Next.jsの画像最適化パイプラインはネイティブアドオンのsharpを使用する。Bunで動作するが、時折問題が発生した。
- ISR(増分静的再生成) — 動作するが、Bunでの本番環境でのストレステストは行っていない。
Next.jsに対する私の推奨#
パッケージマネージャーにBunを使う。ランタイムにNode.jsを使う。これにより、互換性リスクなしに bun install の速度メリットが得られる。
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start"
}
}# 日常のワークフロー
bun install # 高速パッケージインストール
bun run dev # Node.js経由で「next dev」を実行
bun run build # Node.js経由で「next build」を実行BunのNode.js互換性がNext.jsの内部使用に対して100%に達したら(近いがまだ到達していない)、切り替える。それまでは、パッケージマネージャーだけでインストールを正当化できる十分な時間の節約がある。
DockerでのBun#
公式Bun Dockerイメージはよく管理されており、本番環境対応だ。
基本的なDockerfile#
FROM oven/bun:1 AS base
WORKDIR /app
# 依存関係のインストール
FROM base AS deps
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
# ビルド(必要な場合)
FROM base AS build
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
# 本番環境
FROM base AS production
WORKDIR /app
# rootとして実行しない
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 appuser
USER appuser
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./
EXPOSE 3000
CMD ["bun", "run", "dist/server.js"]最小イメージのためのマルチステージビルド#
# ビルドステージ:全依存関係を含むフルBunイメージ
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build ./src/index.ts --target bun --outdir ./dist --minify
# ランタイムステージ:より小さなベースイメージ
FROM oven/bun:1-slim AS runtime
WORKDIR /app
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 appuser
USER appuser
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["bun", "run", "dist/index.js"]単一バイナリへのコンパイル#
これはデプロイメントにおけるBunのキラー機能の1つだ:
# アプリを単一の実行ファイルにコンパイル
bun build --compile ./src/server.ts --outfile server
# 出力はスタンドアロンバイナリ — 実行にBunもNode.jsも不要
./server# コンパイル済みバイナリを使った超最小Dockerイメージ
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build --compile ./src/server.ts --outfile server
# 最終イメージ — バイナリだけ
FROM debian:bookworm-slim
WORKDIR /app
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 appuser
USER appuser
COPY --from=builder /app/server ./server
EXPOSE 3000
CMD ["./server"]コンパイル済みバイナリは通常50〜90 MB(Bunランタイムをバンドル)。Goバイナリより大きいが、フルNode.jsインストール + node_modules よりはるかに小さい。コンテナ化デプロイメントでは、自己完結型の特性は大幅な簡素化だ。
サイズ比較#
# Node.jsイメージ
docker images | grep node
# node:20-slim 〜180MB
# Bunイメージ
docker images | grep bun
# oven/bun:1-slim 〜130MB
# debian:bookworm-slim上のコンパイル済みバイナリ
# 〜80MBベース + 〜70MBバイナリ = 合計〜150MB
# Alpine + Node.js
# node:20-alpine 〜130MB + node_modulesバイナリアプローチは最終イメージから node_modules を完全に排除する。本番環境での npm install なし。数百パッケージからのサプライチェーン攻撃面なし。ファイル1つだけだ。
移行パターン#
Bunへの移行を検討しているなら、私が推奨する段階的なパスはこうだ:
フェーズ1:パッケージマネージャーのみ(リスクゼロ)#
# npm/yarn/pnpmをbun installに置き換え
# CIパイプラインを変更:
# 変更前:
npm ci
# 変更後:
bun install --frozen-lockfileコード変更なし。ランタイム変更なし。インストールが速くなるだけ。もし何か壊れたら(壊れないが)、bun.lockb を削除して npm install を実行すれば元に戻る。
フェーズ2:スクリプトとツール#
# 開発スクリプトにbunを使用
bun run dev
bun run lint
bun run format
# ワンオフスクリプトにbunを使用
bun run scripts/seed-database.ts
bun run scripts/migrate.ts実際のアプリケーションのランタイムとしてはまだNode.jsを使用。しかしスクリプトはBunのより速い起動とネイティブTypeScriptサポートの恩恵を受ける。
フェーズ3:テストランナー(中リスク)#
# シンプルなテストスイートでvitest/jestをbun testに置き換え
bun test
# 複雑なテストセットアップではvitestを維持
# (Testing Library、MSW、カスタム環境)bun test で完全なテストスイートを実行する。すべてパスすれば、devDependencyを1つ排除できた。互換性の問題で一部のテストが失敗する場合、それらにはVitestを維持し、残りには bun test を使用する。
フェーズ4:新サービスのランタイム(計算されたリスク)#
// 新しいマイクロサービスやAPI — 最初からBunで始める
Bun.serve({
port: 3000,
fetch(req) {
// 新しいサービスをここに
},
});既存のNode.jsサービスをBunランタイムに移行しない。代わりに、最初からBunで新しいサービスを書く。これにより影響範囲が限定される。
フェーズ5:ランタイム移行(上級者向け)#
# 徹底的なテスト後にのみ:
# 既存サービスでnodeをbunに置き換え
# 変更前:
node dist/server.js
# 変更後:
bun dist/server.js優れたテストカバレッジを持つサービスにのみこれを推奨する。本番環境を切り替える前に、Bunで負荷テストを実行すること。
環境変数と設定#
Bunは .env ファイルを自動的に処理する — dotenv パッケージは不要:
# .env
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=sk-test-12345
PORT=3000// インポートなしでこれらが利用可能
console.log(process.env.DATABASE_URL);
console.log(process.env.API_KEY);
console.log(Bun.env.PORT); // Bun固有の代替手段Bunは .env、.env.local、.env.production などを自動的に読み込み、Next.jsと同じ慣例に従う。package.json の依存関係が1つ減る。
エラーハンドリングとデバッグ#
Bunのエラー出力は大幅に改善されたが、一部のケースではまだNode.jsほど洗練されていない:
# Bunのデバッガー — VS Codeで動作
bun --inspect run server.ts
# Bunのinspect-brk — 最初の行で一時停止
bun --inspect-brk run server.tsVS Codeでは、.vscode/launch.json に以下を追加:
{
"version": "0.2.0",
"configurations": [
{
"type": "bun",
"request": "launch",
"name": "Debug Bun",
"program": "${workspaceFolder}/src/server.ts",
"cwd": "${workspaceFolder}",
"stopOnEntry": false,
"watchMode": false
}
]
}BunのスタックトレースはTypeScriptのソースマップを含み、一般的に正確だ。主なデバッグのギャップは、一部のNode.js固有のデバッグツール(ndb や clinic.js など)がBunで動作しないことだ。
セキュリティの考慮事項#
本番環境でBunを評価している場合に考えるべきこと:
成熟度:Node.jsは15年以上本番環境で使われてきた。HTTPパース、TLS処理、ストリーム処理のすべてのエッジケースが発見され修正されている。Bunはより若い。十分にテストされているが、未発見のバグの表面積が大きい。
セキュリティパッチ:Bunチームは頻繁にアップデートを出すが、Node.jsのセキュリティチームは正式なCVEプロセス、協調的な開示、より長い実績がある。セキュリティクリティカルなアプリケーションでは、これが重要だ。
サプライチェーン:Bunの組み込み機能(SQLite、HTTPサーバー、WebSocket)はnpm依存関係が少なくなることを意味する。依存関係が少なければ、サプライチェーン攻撃の表面積が小さくなる。これは本当のセキュリティ上の利点だ。
# 依存関係数の比較
# 典型的なExpress + SQLite + WebSocketプロジェクト:
npm ls --all | wc -l
# 〜340パッケージ
# Bunの組み込み機能で同じ機能性:
bun pm ls --all | wc -l
# 〜12パッケージ(アプリケーションコードだけ)本番ワークロードに信頼するパッケージ数の意味のある削減だ。
パフォーマンスチューニング#
Bun固有のパフォーマンスのヒント:
// 本番チューニング用のBun.serve()オプション
Bun.serve({
port: 3000,
// 最大リクエストボディサイズを増加(デフォルトは128MB)
maxRequestBodySize: 1024 * 1024 * 50, // 50MB
// 開発モードを有効にしてより良いエラーページを表示
development: process.env.NODE_ENV !== "production",
// ポート再利用(ゼロダウンタイムリスタートに便利)
reusePort: true,
fetch(req) {
return new Response("OK");
},
});// ランタイムコード変換にBun.Transpilerを使用
const transpiler = new Bun.Transpiler({
loader: "tsx",
target: "browser",
});
const code = transpiler.transformSync(`
const App: React.FC = () => <div>Hello</div>;
export default App;
`);# Bunのメモリ使用フラグ
bun --smol run server.ts # メモリフットプリントを削減(やや遅くなる)
# 最大ヒープサイズを設定
BUN_JSC_forceRAMSize=512000000 bun run server.ts # 〜512MBリミットよくある落とし穴#
Bunを1年使って、つまずいたこと:
1. グローバルFetchの動作の違い#
// Node.js 18+のfetchとBunのfetchは
// ヘッダーやリダイレクトの処理がわずかに異なる
// Bunはデフォルトでリダイレクトに従う(ブラウザと同様)
// Node.jsのfetchもリダイレクトに従うが、
// 特定のステータスコード(303、307、308)での動作が異なる場合がある
const response = await fetch("https://api.example.com/data", {
redirect: "manual", // リダイレクト処理を明示的にする
});2. プロセス終了の動作#
// Bunはイベントループが空になると終了する
// Node.jsは残存するハンドルのため実行を継続する場合がある
// Bunスクリプトが予期せず終了する場合、
// イベントループを維持するものがない
// これはBunで即座に終了する:
setTimeout(() => {}, 0);
// これは実行を継続する:
setTimeout(() => {}, 1000);
// (Bunはタイムアウト発火後に終了する)3. TypeScript設定#
// Bunは独自のtsconfigデフォルトを持つ
// BunとNode.jsの間でプロジェクトを共有する場合、
// tsconfig.jsonで明示的に指定する:
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["bun-types"] // Bun型定義を追加
}
}# Bun型をインストール
bun add -d @types/bun4. 開発時のホットリロード#
# Bunには組み込みウォッチモードがある
bun --watch run server.ts
# ファイル変更時にプロセスを再起動する
# HMR(ホットモジュールリプレースメント)ではない — 完全な再起動
# しかしBunの起動が速いため、即座に感じる5. bunfig.toml 設定ファイル#
# bunfig.toml — Bunの設定ファイル(オプション)
[install]
# プライベートレジストリを使用
registry = "https://npm.mycompany.com"
# スコープ付きレジストリ
[install.scopes]
"@mycompany" = "https://npm.mycompany.com"
[test]
# テスト設定
coverage = true
coverageReporter = ["text", "lcov"]
[run]
# bun runに使用するシェル
shell = "bash"私の結論#
1年間の本番使用を経て、落ち着いたところ:
現在Bunを使っている場面#
すべてのプロジェクトのパッケージマネージャー — このNext.jsブログを含む。bun install は速く、互換性は本質的に完璧だ。もうnpmやyarnを使う理由がない。pnpmが唯一検討する代替手段だ(モノレポでの厳格な依存関係解決のため)。
スクリプトとCLIツールのランタイム — 一度実行する必要があるTypeScriptファイルは、すべて bun で実行する。コンパイルステップなし。高速起動。組み込み .env 読み込み。ワークフローで ts-node と tsx を完全に置き換えた。
小さなAPIと社内ツールのランタイム — Bun.serve() + bun:sqlite は、社内ツール、Webhookハンドラー、小さなサービスのための信じられないほど生産的なスタックだ。「1バイナリ、依存関係なし」のデプロイメントモデルは魅力的だ。
シンプルなプロジェクトのテストランナー — テストニーズが単純なプロジェクトでは、bun test は高速でゼロ設定だ。
Node.jsを続ける場面#
本番Next.js — Bunが動かないからではなく、リスクとリターンのバランスがまだ正当化できないから。Next.jsは多くの統合ポイントを持つ複雑なフレームワークだ。その下には最も実戦検証済みのランタイムが欲しい。
重要な本番サービス — メインのAPIサーバーはPM2の背後でNode.jsを実行している。モニタリングエコシステム、デバッグツール、運用知識 — すべてNode.jsだ。Bunはそこに到達するだろうが、まだそこにはない。
ネイティブアドオンを含むもの — 依存関係チェーンにC++ネイティブアドオンが含まれる場合、Bunを試すことすらしない。互換性の問題のデバッグは価値がない。
Bunに慣れていないチーム — Bunをランタイムとしてチームに導入すると、認知的オーバーヘッドが増える。パッケージマネージャーとしてなら問題ない。ランタイムとしてなら、チームの準備ができるまで待つ。
注目していること#
Bunの互換性トラッカー — 私が気にするNode.js APIに対して100%に達したら、再評価する。
フレームワークサポート — Next.js、Remix、SvelteKitはすべてBunサポートのレベルが異なる。そのどれかが公式にBunを本番ランタイムとしてサポートした時、それがシグナルだ。
エンタープライズ採用 — 実際のSLAを持つ企業がBunを本番環境で実行し、それについて書くようになれば、成熟度の疑問は解消される。
1.2+リリースライン — Bunは急速に進化している。毎週機能が追加される。今使っているBunは、1年前に試したBunよりも意味のある改善がある。
まとめ#
Bunは万能薬ではない。遅いアプリを速くすることも、設計が悪いAPIを良い設計にすることもない。しかし、JavaScriptエコシステムの開発者体験の真の改善だ。
Bunで最も評価しているのは、どの単一の機能でもない。ツールチェーンの複雑さの削減だ。パッケージをインストールし、TypeScriptを実行し、コードをバンドルし、テストを実行する1つのバイナリ。スクリプト用の tsconfig.json なし。Babelなし。別のテストランナー設定なし。bun run your-file.ts とすれば動く。
実践的なアドバイス:bun install から始めよう。リスクゼロ、即座の効果。次にスクリプトで bun run を試す。残りは具体的なニーズに基づいて評価する。オールインする必要はない。Bunは部分的な置換として完璧に機能し、それがおそらくほとんどの人が今日使うべき方法だ。
JavaScriptランタイムの景色はBunがあることで良くなっている。競争はNode.jsもより良くしている — Node.js 22+は大幅に速くなった。その一因はBunからのプレッシャーだ。全員が勝者だ。