•21 min read

Node.jsからBun 1.2への本番環境移行:フルスタックHTTP、SQLite、パッケージパフォーマンス

Node.jsからBun 1.2への本番環境移行:フルスタックHTTP、SQLite、パッケージパフォーマンス

このガイドでは、既存のNode.jsマイクロサービスを本番環境デプロイのためにBun 1.2へ移行するプロセスを詳しく説明します。HTTPサーバーの移行、bun:sqliteの統合、WebSocketサーバーの実装、npmパッケージの互換性、ネイティブC++アドオンに関する考慮事項、Node.js APIのギャップへの対処、そしてパフォーマンスベンチマークを含むDockerコンテナ化について解説します。

Bun 1.2が本番環境にもたらす主な利点

Bun 1.2は、その基盤となるZig実装とJavaScriptCoreエンジンにより、Node.jsと比較して大幅なパフォーマンス上の利点を提供します。主な利点は以下の通りです。

  1. より速い起動時間: サーバーレス関数や頻繁にスケールされるマイクロサービスにとって重要です。
  2. メモリフットプリントの削減: 運用コストを低減し、ホストあたりの密度を高めます。
  3. 統合されたツール: bun install、bun run、bun test、およびbun buildは開発ワークフローを効率化します。
  4. ネイティブAPI: bun:sqlite、bun:ffi、bun:serveは高度に最適化されたプリミティブを提供します。
Advertisement

HTTPサーバーの移行

BunのネイティブHTTPサーバー(Bun.serve)は、Node.jsのhttpモジュールやExpressのようなフレームワークに代わる高性能な選択肢です。最小限のオーバーヘッドで設計されています。

Node.js (Express) の例

// src/node-server.ts
import express from 'express';
import { readFileSync } from 'fs';
import path from 'path';

const app = express();
const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;

app.use(express.json());

app.get('/', (req, res) => {
  res.send('Hello from Node.js Express!');
});

app.get('/data', (req, res) => {
  try {
    const data = readFileSync(path.join(__dirname, '../data.json'), 'utf8');
    res.json(JSON.parse(data));
  } catch (error) {
    console.error('Error reading data:', error);
    res.status(500).send('Internal Server Error');
  }
});

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

Bun 1.2 (Bun.serve) の同等な例

Bun.serveへの移行には、リクエスト/レスポンスの処理を適応させる必要があります。BunのRequestとResponseオブジェクトは、Web標準のRequestとResponseオブジェクトです。

// src/bun-server.ts
import { readFileSync } from 'fs';
import path from 'path';

const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;

const server = Bun.serve({
  port,
  async fetch(req: Request): Promise<Response> {
    const url = new URL(req.url);

    if (url.pathname === '/') {
      return new Response('Hello from Bun!', { status: 200 });
    }

    if (url.pathname === '/data') {
      try {
        const dataPath = path.join(import.meta.dir, '../data.json');
        const data = readFileSync(dataPath, 'utf8');
        return new Response(data, {
          headers: { 'Content-Type': 'application/json' },
          status: 200,
        });
      } catch (error) {
        console.error('Error reading data:', error);
        return new Response('Internal Server Error', { status: 500 });
      }
    }

    // Example for POST request with JSON body
    if (url.pathname === '/submit' && req.method === 'POST') {
      try {
        const body = await req.json();
        console.log('Received JSON body:', body);
        return new Response(JSON.stringify({ message: 'Data received', data: body }), {
          headers: { 'Content-Type': 'application/json' },
          status: 200,
        });
      } catch (error) {
        console.error('Error parsing JSON body:', error);
        return new Response('Bad Request: Invalid JSON', { status: 400 });
      }
    }

    return new Response('Not Found', { status: 404 });
  },
  error(error: Error): Response {
    console.error('Bun server error:', error);
    return new Response('Internal Server Error', { status: 500 });
  },
});

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

パス解決にimport.meta.dirを使用している点に注目してください。これはNode.jsの__dirnameに相当するBunの機能です。

bun:sqliteによるネイティブSQLite

Bunは、bun:sqliteを介して高度に最適化されたネイティブSQLiteクライアントを提供します。これにより、ネイティブC++アドオンやコンパイル手順を伴うことが多いsqlite3やbetter-sqlite3のような外部npmパッケージは不要になります。

Node.js (better-sqlite3) の例

// src/node-sqlite.ts
import Database from 'better-sqlite3';
import path from 'path';

const dbPath = path.join(__dirname, '../data.db');
const db = new Database(dbPath);

db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL
  );
`);

export function insertUser(name: string, email: string) {
  const stmt = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
  const info = stmt.run(name, email);
  return info.lastInsertRowid;
}

export function getUsers() {
  const stmt = db.prepare('SELECT * FROM users');
  return stmt.all();
}

// Example usage
if (require.main === module) {
  insertUser('Alice', 'alice@example.com');
  insertUser('Bob', 'bob@example.com');
  console.log('Users:', getUsers());
  db.close();
}

Bun 1.2 (bun:sqlite) の同等な例

bun:sqliteは同様のAPIサーフェスを提供するため、移行は簡単です。

// src/bun-sqlite.ts
import { Database } from 'bun:sqlite';
import path from 'path';

const dbPath = path.join(import.meta.dir, '../data.db');
const db = new Database(dbPath);

db.run(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL
  );
`);

export function insertUser(name: string, email: string) {
  const query = db.query('INSERT INTO users (name, email) VALUES (?, ?)');
  const result = query.run(name, email);
  // Bun's result object for INSERT operations might differ slightly
  // For SQLite, lastInsertRowid is often available on the database object or via PRAGMA
  // For simplicity, we'll assume a successful run implies insertion.
  // A more robust solution would query for the last_insert_rowid()
  const lastIdQuery = db.query('SELECT last_insert_rowid() as id;');
  return (lastIdQuery.get() as { id: number }).id;
}

export function getUsers() {
  const query = db.query('SELECT * FROM users');
  return query.all();
}

// Example usage
if (import.meta.main) {
  insertUser('Charlie', 'charlie@example.com');
  insertUser('David', 'david@example.com');
  console.log('Users:', getUsers());
  db.close();
}

ネイティブWebSocketサーバー

BunのBun.serveはWebSocketのファーストクラスサポートも含まれており、高性能で統合されたソリューションを提供します。

Node.js (wsライブラリ) の例

// src/node-ws.ts
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', ws => {
  console.log('Client connected (Node.js WS)');
  ws.on('message', message => {
    console.log(`Received: ${message}`);
    ws.send(`Echo: ${message}`);
  });
  ws.on('close', () => console.log('Client disconnected (Node.js WS)'));
  ws.on('error', error => console.error('WebSocket error (Node.js WS):', error));
});

console.log('Node.js WebSocket server listening on port 8080');

Bun 1.2 (Bun.serve WebSocket) の同等な例

BunのWebSocket APIは、websocket設定オブジェクトを使用して、Bun.serveに直接統合されています。

// src/bun-ws.ts
const server = Bun.serve({
  port: 8080,
  fetch(req, server) {
    const url = new URL(req.url);
    if (url.pathname === '/ws') {
      const success = server.upgrade(req, {
        data: {
          // Optional: attach any data to the WebSocket connection
          connectedAt: Date.now(),
        },
      });
      if (success) {
        return; // Bun handles the WebSocket connection
      }
    }
    return new Response('Not Found', { status: 404 });
  },
  websocket: {
    open(ws) {
      console.log(`Client connected (Bun WS). Connected at: ${ws.data.connectedAt}`);
      ws.send('Welcome to Bun WebSocket!');
    },
    message(ws, message) {
      console.log(`Received: ${message}`);
      ws.send(`Echo: ${message}`);
    },
    close(ws, code, message) {
      console.log(`Client disconnected (Bun WS). Code: ${code}, Message: ${message}`);
    },
    error(ws, error) {
      console.error('WebSocket error (Bun WS):', error);
    },
    // Optional: perMessageDeflate, maxPayloadLength, idleTimeout
    // idleTimeout: 30, // seconds
  },
});

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

npmパッケージの互換性とネイティブC++アドオン

Bunは既存のnpmパッケージとの高い互換性を目指しています。ほとんどの純粋なJavaScriptパッケージは変更なしで動作します。

ネイティブC++アドオン

互換性が課題となるのはここです。Node.jsのネイティブアドオン(.nodeファイル)は、Node.jsのN-API(Node-API)およびV8エンジンに対してコンパイルされます。BunはJavaScriptCoreと独自のFFI(Foreign Function Interface)を使用してネイティブな相互作用を行います。

  • 直接的な互換性: Node.js用にコンパイルされたネイティブC++アドオンは、通常、Bunと直接的な互換性はありません。
  • 回避策:
    • 純粋なJSの代替: ネイティブアドオンに依存するパッケージの純粋なJavaScriptの代替を探します。
    • bun:ffi: 重要なパフォーマンスが要求される操作の場合、Bun用に特別にコンパイルされた共有ライブラリ(.so、.dylib、.dll)を呼び出すために、bun:ffiを使用してネイティブロジックを書き直す必要があるかもしれません。これは複雑な作業です。
    • コンテナ化: 特定のネイティブアドオンが不可欠で書き換えられない場合、その特定のマイクロサービスをNode.jsコンテナで実行し、RPCを介して通信することを検討できます。これはBunの利点の一部を打ち消しますが、機能性を保証します。

例: bcrypt (ネイティブアドオン)

bcryptは、パフォーマンスのためにネイティブC++アドオンを使用する一般的なパッケージです。

// src/node-bcrypt.ts
import bcrypt from 'bcrypt';

async function hashPassword(password: string) {
  const saltRounds = 10;
  const hashedPassword = await bcrypt.hash(password, saltRounds);
  console.log('Hashed password (Node.js):', hashedPassword);
  return hashedPassword;
}

async function verifyPassword(password: string, hash: string) {
  const isMatch = await bcrypt.compare(password, hash);
  console.log('Password match (Node.js):', isMatch);
  return isMatch;
}

if (require.main === module) {
  (async () => {
    const hash = await hashPassword('mysecretpassword');
    await verifyPassword('mysecretpassword', hash);
    await verifyPassword('wrongpassword', hash);
  })();
}

bun install bcryptを実行すると、Bunはそれをインストールしようとします。ネイティブアドオンのビルドに失敗した場合、エラーが表示されます。このような場合、純粋なJSの代替を使用するか、別のアプローチが必要になることがあります。

Bun 1.2 (bcrypt と潜在的な問題)

Bunは、正しいアーキテクチャとN-APIバージョン用にプリコンパイルされていれば、Node.jsのネイティブアドオンを実行できる場合もありますが、これは保証されておらず、しばしば失敗します。より安全なアプローチは、純粋なJSの代替または別のライブラリを使用することです。

bcryptの場合、bcryptjsのような純粋なJS実装は、速度は遅くなりますが、多くの場合実行可能な代替手段となります。

// src/bun-bcryptjs.ts
import bcrypt from 'bcryptjs'; // Using bcryptjs for Bun compatibility

async function hashPassword(password: string) {
  const saltRounds = 10;
  const hashedPassword = await bcrypt.hash(password, saltRounds);
  console.log('Hashed password (Bun/bcryptjs):', hashedPassword);
  return hashedPassword;
}

async function verifyPassword(password: string, hash: string) {
  const isMatch = await bcrypt.compare(password, hash);
  console.log('Password match (Bun/bcryptjs):', isMatch);
  return isMatch;
}

if (import.meta.main) {
  (async () => {
    const hash = await hashPassword('mysecretpassword');
    await verifyPassword('mysecretpassword', hash);
    await verifyPassword('wrongpassword', hash);
  })();
}

Node.js APIのギャップと緩和策

BunはNode.jsとの高い互換性を目指していますが、一部のAPIは完全に実装されていないか、動作が異なります。

  • child_process: exec、spawn、forkなどの最も一般的な関数はサポートされています。エッジケースや特定のオプションは異なる場合があります。
  • vmモジュール: サポートが限定的です。アプリケーションがvm.runInContextなどに大きく依存している場合、これは障害となる可能性があります。
  • domainモジュール: Node.jsでは非推奨であり、Bunではサポートされていません。
  • clusterモジュール: Bunにはclusterの直接的な同等物はありません。マルチコア利用の場合、通常は複数のBunプロセスを実行し、ロードバランサーを使用するか、特定のタスクにBunの組み込みマルチスレッドを活用します(ただし、clusterのようにHTTPサーバーのスケーリングには使用しません)。
  • fsモジュール: ほぼ互換性がありますが、あまり一般的でないオプションや同期バージョンには微妙な違いがあるかもしれません。ファイルシステムを多用する操作は常にテストしてください。
  • net / tls: 基本的なクライアント/サーバー機能は存在しますが、高度な設定には調整が必要な場合があります。

緩和戦略:

  1. 包括的なテスト: APIのギャップを特定するには、単体テスト、統合テスト、エンドツーエンドテストが不可欠です。
  2. ポリフィル/シム: 軽微なギャップには、小さなポリフィルが可能な場合があります。
  3. リファクタリング: 重要なギャップがある場合は、問題のあるコードをWeb標準APIまたはBunのネイティブAPIを使用するようにリファクタリングします。
  4. 機能フラグ: 問題のあるコードパスを機能フラグで分離し、段階的な移行を可能にします。

Dockerコンテナ化のベンチマーク

Bunアプリケーションのコンテナ化は簡単です。公式のoven/bun Dockerイメージは、軽量なベースを提供します。

Node.js用Dockerfile

# Dockerfile.node
FROM node:20-alpine AS base

WORKDIR /app

COPY package.json bun.lockb* ./
RUN npm install --frozen-lockfile

COPY . .

EXPOSE 3000
CMD ["node", "src/node-server.ts"]

Bun用Dockerfile

# Dockerfile.bun
FROM oven/bun:1.2.0-alpine AS base

WORKDIR /app

COPY package.json bun.lockb* ./
# Bun automatically installs dependencies on 'bun run' if not present,
# but explicit install is good practice for build stage.
RUN bun install --frozen-lockfile

COPY . .

EXPOSE 3000
CMD ["bun", "run", "src/bun-server.ts"]

ベンチマーク方法論

単純なHTTPサーバーのイメージサイズとランタイムメモリ使用量を比較します。

  1. イメージのビルド:
    docker build -t node-app -f Dockerfile.node .
    docker build -t bun-app -f Dockerfile.bun .
    
  2. イメージサイズ:
    docker images | grep "node-app\|bun-app"
    
  3. ランタイムメモリ (docker statsを使用):
    docker run -d --name node-server -p 3001:3000 node-app
    docker run -d --name bun-server -p 3002:3000 bun-app
    docker stats --no-stream node-server bun-server
    # Stop containers after collecting stats
    docker stop node-server bun-server && docker rm node-server bun-server
    

ベンチマーク結果(例示)

メトリックNode.js (20-alpine)Bun (1.2.0-alpine)注記
イメージサイズ約180 MB約100 MBBunのベースイメージは大幅に小さい。
起動時間約500 ms約50 msBunは桁違いに速い。
メモリ使用量約30 MB約10 MB単純なHTTPサーバーの場合、Bunは少ない。
RPS (ab -c 50 -n 10000)約2500 RPS約7000 RPSBun.serveは高度に最適化されている。

注: これらは例示的なベンチマークです。実際の結果は、アプリケーションの複雑さ、ワークロード、ハードウェアに大きく依存します。

本番環境での落とし穴とトラブルシューティング

  1. Error: Cannot find module '...':

    • 原因: Bunのモジュール解決は一般的に互換性がありますが、特定の特殊なケースや、tsconfig.jsonパスがBun用に正しく設定されていない場合に異なることがあります。
    • 修正:
      • tsconfig.json pathsが正しくマッピングされ、baseUrlが設定されていることを確認します。
      • インポートされたパッケージのpackage.json exportsフィールドを確認します。
      • 混在して使用している場合は、不足しているbun installまたはnpm installがないか確認します。
      • Bunのnode_modules解決はより厳密な場合があります。すべての依存関係が明示的にリストされていることを確認します。
      • require()を使用している場合は、ファイル拡張子が存在することを確認します(例: require('./module.js'))。
  2. ネイティブアドオンの失敗:

    • 原因: Node.jsのネイティブC++アドオン(.nodeファイル)をBunで直接使用しようとしている。
    • 修正:
      • 問題のあるパッケージを特定します。
      • 純粋なJavaScriptの代替を探します(例: bcryptの代わりにbcryptjs)。
      • 代替がない場合は、その機能を別のNode.jsマイクロサービスに分離するか、bun:ffiで再実装することを検討します(上級者向け)。
  3. process.envの違い:

    • 原因: Bunのprocess.envはほぼ互換性がありますが、Node.js固有の環境変数の一部が存在しないか、動作が異なる場合があります。
    • 修正:
      • デプロイ環境で必要な環境変数を明示的に定義します。
      • Node.jsに非常に特化した内部環境変数に依存しないようにします。
  4. Buffer APIの不整合:

    • 原因: BunはBufferをサポートしていますが、その基盤となる実装は、特に古くてあまり一般的でないBufferメソッドにおいて、Node.jsのものと微妙に異なる場合があります。
    • 修正:
      • 可能な限りWeb標準のUint8ArrayとTextEncoder/TextDecoderを優先します。
      • Buffer操作を含むコードパスを徹底的にテストします。
  5. fs.watch / fs.watchFileの動作:

    • 原因: ファイルシステムの監視はプラットフォームに依存し、Node.jsとBunの間で異なるパフォーマンス特性やイベントトリガーを持つことがあります。
    • 修正:
      • ターゲットの本番環境でファイル監視を広範囲にテストします。
      • ネイティブの動作が一貫しない場合は、外部のファイル監視ソリューションを検討します。
  6. Bun.serve vs. Node.js HTTPサーバーのkeepAliveTimeout:

    • 原因: BunのBun.serveにはWebSocket用のidleTimeoutと一般的な接続タイムアウトがあります。Node.jsにはkeepAliveTimeoutがあります。デフォルト値と動作が異なる場合があります。
    • 修正:
      • WebSocketの場合は、Bun.serveでidleTimeoutを明示的に設定します。
      • HTTPの場合は、ロードバランサーまたはプロキシが接続タイムアウトを適切に処理することを確認するか、必要に応じてfetchハンドラー内でカスタムタイムアウトロジックを実装します。

よくある質問

  1. Q: ExpressやNestJSのような既存のNode.jsフレームワークをBunで使用できますか?

    • A: はい、概ね可能です。BunはNode.jsとの高い互換性を目指しているため、多くのフレームワークが動作します。ただし、HTTP処理においてBun.serveの完全なパフォーマンス上の利点を得ることはできません。最適なパフォーマンスを得るには、HTTPエンドポイントを直接Bun.serveに移行するか、Bunネイティブのフレームワークを使用してください。bun installは通常、フレームワークの依存関係を処理します。
  2. Q: Bunは本番環境でTypeScriptのコンパイルをどのように処理しますか?

    • A: Bunには組み込みのTypeScriptトランスパイラがあります。.tsファイルをbun run src/index.tsで直接実行できます。本番環境では、bun buildがTypeScriptをJavaScriptにコンパイルし、それを実行できます。これにより、tscやts-nodeは不要になります。
  3. Q: SQLite以外のデータベースドライバー(例: PostgreSQL、MySQL)はどうですか?

    • A: ほとんどのリレーショナルデータベースでは、既存のnpmパッケージ(例: pg、mysql2、sequelize、prisma)を使用します。これらは通常、純粋なJavaScriptであるか、標準のネットワークプロトコルに依存しているため、Bunでもうまく動作します。bun installがそれらの依存関係を管理します。
  4. Q: bun:ffiはすべてのネイティブNode.jsアドオンの実行可能な代替手段ですか?

    • A: bun:ffiは強力ですが、かなりの労力が必要です。C/C++/Rustコードを記述し、それを共有ライブラリ(.so、.dylib、.dll)にコンパイルし、その後、bun:ffiが呼び出すための関数シグネチャをTypeScriptで定義する必要があります。これは、パフォーマンスが重要な分離されたネイティブロジックのソリューションであり、複雑なNode.jsアドオンのドロップイン代替ではありません。
  5. Q: 本番環境でBunアプリケーションをデバッグするにはどうすればよいですか?

    • A: BunはChrome DevTools Protocolをサポートしています。--inspectまたは--inspect-brk(例: bun --inspect src/index.ts)でBunを起動し、デバッガー(VS CodeのデバッガーやChrome DevToolsなど)を接続できます。これにより、Node.jsと同様の使い慣れたデバッグ体験が提供されます。
Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement