デザインについての学習メモブログ

Node.js入門(6):パフォーマンス最適化テクニック

記事内に広告が含まれています。

Node.js入門(6):パフォーマンス最適化テクニック

Node.jsアプリケーションのパフォーマンスを最大限に引き出すための実践的なテクニックを学びます。

メモリ管理、並列処理、キャッシング、プロファイリングなど、本番環境で役立つ知識を網羅します。

💡この記事でわかる事

以下の内容を習得することができます:

  1. パフォーマンス測定の基礎
  2. 非同期処理の最適化
  3. メモリ管理とガベージコレクション
  4. クラスタリングによるマルチコア活用
  5. Worker Threadsによる並列処理
  6. キャッシング戦略
  7. データベースクエリの最適化
  8. プロファイリングとデバッグ

1. パフォーマンス測定の基礎

最適化の前に、まず現状を測定することが重要です。

1.1 実行時間の測定

Node.jsには複数の時間測定方法があります。

JavaScript
// console.time() / console.timeEnd()
console.time('処理時間');
// 何か重い処理
for (let i = 0; i < 1000000; i++) {
  Math.sqrt(i);
}
console.timeEnd('処理時間');
// => 処理時間: 5.234ms

// performance.now() - 高精度
const { performance } = require('perf_hooks');

const start = performance.now();
// 処理
const end = performance.now();
console.log(`実行時間: ${(end - start).toFixed(3)}ms`);

// process.hrtime.bigint() - ナノ秒精度
const startTime = process.hrtime.bigint();
// 処理
const endTime = process.hrtime.bigint();
const duration = endTime - startTime;
console.log(`実行時間: ${Number(duration) / 1000000}ms`);

1.2 ベンチマークの実装

複数の実装を比較するベンチマークツールです。

JavaScript
class Benchmark {
  constructor(name) {
    this.name = name;
    this.results = [];
  }

  async run(fn, iterations = 1000) {
    const times = [];
    
    // ウォームアップ(JITコンパイル最適化のため)
    for (let i = 0; i < 10; i++) {
      await fn();
    }
    
    // 実測定
    for (let i = 0; i < iterations; i++) {
      const start = performance.now();
      await fn();
      const end = performance.now();
      times.push(end - start);
    }
    
    // 統計情報を計算
    const sorted = times.sort((a, b) => a - b);
    const avg = times.reduce((a, b) => a + b) / times.length;
    const median = sorted[Math.floor(sorted.length / 2)];
    const min = sorted[0];
    const max = sorted[sorted.length - 1];
    
    return { avg, median, min, max, iterations };
  }

  async compare(tests) {
    console.log(`\n=== ${this.name} ===\n`);
    
    for (const [name, fn] of Object.entries(tests)) {
      const result = await this.run(fn, 1000);
      console.log(`${name}:`);
      console.log(`  平均: ${result.avg.toFixed(3)}ms`);
      console.log(`  中央値: ${result.median.toFixed(3)}ms`);
      console.log(`  最小: ${result.min.toFixed(3)}ms`);
      console.log(`  最大: ${result.max.toFixed(3)}ms`);
      console.log('');
    }
  }
}

// 使用例
const bench = new Benchmark('配列操作の比較');

await bench.compare({
  'for文': () => {
    const arr = [];
    for (let i = 0; i < 1000; i++) {
      arr.push(i * 2);
    }
  },
  'map': () => {
    const arr = Array.from({ length: 1000 }, (_, i) => i * 2);
  },
  'whileループ': () => {
    const arr = [];
    let i = 0;
    while (i < 1000) {
      arr.push(i * 2);
      i++;
    }
  }
});

1.3 メモリ使用量の測定

JavaScript
function measureMemory(fn, label) {
  // ガベージコレクションを強制実行(要 --expose-gc フラグ)
  if (global.gc) {
    global.gc();
  }
  
  const before = process.memoryUsage();
  
  fn();
  
  const after = process.memoryUsage();
  
  console.log(`\n=== ${label} ===`);
  console.log(`ヒープ使用量: ${((after.heapUsed - before.heapUsed) / 1024 / 1024).toFixed(2)} MB`);
  console.log(`外部メモリ: ${((after.external - before.external) / 1024 / 1024).toFixed(2)} MB`);
}

// 使用例
measureMemory(() => {
  const arr = new Array(1000000).fill('test');
}, '100万要素の配列');

// 実行時: node --expose-gc script.js

2. 非同期処理の最適化

2.1 Promise.all() vs 逐次実行

並列実行可能な処理は、Promise.all()で高速化できます。

JavaScript
const { performance } = require('perf_hooks');

// シミュレーション用の非同期関数
function fetchData(id) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ id, data: `データ${id}` });
    }, 100);
  });
}

// 悪い例: 逐次実行(遅い)
async function sequentialFetch() {
  const start = performance.now();
  
  const result1 = await fetchData(1);
  const result2 = await fetchData(2);
  const result3 = await fetchData(3);
  
  const end = performance.now();
  console.log(`逐次実行: ${(end - start).toFixed(0)}ms`);
  // => 約300ms(100ms × 3)
  
  return [result1, result2, result3];
}

// 良い例: 並列実行(速い)
async function parallelFetch() {
  const start = performance.now();
  
  const results = await Promise.all([
    fetchData(1),
    fetchData(2),
    fetchData(3)
  ]);
  
  const end = performance.now();
  console.log(`並列実行: ${(end - start).toFixed(0)}ms`);
  // => 約100ms(並列実行)
  
  return results;
}

// 実行
(async () => {
  await sequentialFetch();
  await parallelFetch();
})();

2.2 並列実行数の制限

大量のリクエストを送る場合、並列数を制限することでリソースを節約できます。

JavaScript
async function fetchWithLimit(urls, limit = 5) {
  const results = [];
  const executing = [];
  
  for (const url of urls) {
    const promise = fetch(url).then(res => res.json());
    results.push(promise);
    
    if (limit <= urls.length) {
      const executing_promise = promise.then(() => {
        executing.splice(executing.indexOf(executing_promise), 1);
      });
      executing.push(executing_promise);
      
      if (executing.length >= limit) {
        await Promise.race(executing);
      }
    }
  }
  
  return Promise.all(results);
}

// より汎用的な実装
class PromisePool {
  constructor(limit) {
    this.limit = limit;
    this.running = 0;
    this.queue = [];
  }

  async add(fn) {
    while (this.running >= this.limit) {
      await new Promise(resolve => this.queue.push(resolve));
    }
    
    this.running++;
    
    try {
      return await fn();
    } finally {
      this.running--;
      const resolve = this.queue.shift();
      if (resolve) resolve();
    }
  }
}

// 使用例
const pool = new PromisePool(3);// 最大3つまで並列実行

const tasks = Array.from({ length: 10 }, (_, i) => 
  pool.add(() => fetchData(i))
);

Promise.all(tasks).then(results => {
  console.log('すべて完了:', results.length);
});

2.3 setImmediate vs process.nextTick

イベントループの仕組みを理解して、適切に使い分けます。

JavaScript
// process.nextTick() - 現在のフェーズの直後に実行
process.nextTick(() => {
  console.log('nextTick');
});

// setImmediate() - チェックフェーズで実行
setImmediate(() => {
  console.log('setImmediate');
});

console.log('同期処理');

// 実行順序:
// => 同期処理
// => nextTick
// => setImmediate

// 再帰的な処理の場合
function recursiveNextTick(count) {
  if (count === 0) return;
  
  process.nextTick(() => {
    console.log('nextTick:', count);
    recursiveNextTick(count - 1);
  });
}

function recursiveImmediate(count) {
  if (count === 0) return;
  
  setImmediate(() => {
    console.log('setImmediate:', count);
    recursiveImmediate(count - 1);
  });
}

// nextTickは他のI/O処理をブロックする可能性がある
// recursiveNextTick(1000); // 危険:I/Oをブロック

// setImmediateは他のI/O処理を妨げない
recursiveImmediate(1000);// 安全:I/Oを妨げない

3. メモリ管理とガベージコレクション

3.1 メモリリークの検出

よくあるメモリリークのパターンとその対策です。

JavaScript
// 悪い例1: グローバル変数の蓄積
global.cache = [];
function addToCache(data) {
  global.cache.push(data);// メモリリーク!
}

// 良い例: 制限付きキャッシュ
class LRUCache {
  constructor(maxSize = 100) {
    this.maxSize = maxSize;
    this.cache = new Map();
  }

  set(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    }
    this.cache.set(key, value);
    
    if (this.cache.size > this.maxSize) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
  }

  get(key) {
    if (!this.cache.has(key)) return null;
    
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value);// 最後尾に移動
    return value;
  }
}

// 悪い例2: イベントリスナーの削除忘れ
const EventEmitter = require('events');
const emitter = new EventEmitter();

function createHandler() {
  const largeData = new Array(1000000).fill('data');
  
  const handler = () => {
    console.log(largeData.length);
  };
  
  emitter.on('event', handler);
  // handlerを削除しないとlargeDataがメモリに残り続ける
}

// 良い例: リスナーを適切に削除
function createHandlerSafe() {
  const largeData = new Array(1000000).fill('data');
  
  const handler = () => {
    console.log(largeData.length);
  };
  
  emitter.on('event', handler);
  
  // クリーンアップ関数を返す
  return () => {
    emitter.removeListener('event', handler);
  };
}

const cleanup = createHandlerSafe();
// 使用後
cleanup();

// 悪い例3: クロージャによるメモリ保持
function createClosure() {
  const largeArray = new Array(1000000).fill('data');
  
  return {
    // largeArrayを参照していないのに、クロージャ全体が保持される
    getValue: () => 'value'
  };
}

// 良い例: 必要なデータのみ保持
function createClosureSafe() {
  const largeArray = new Array(1000000).fill('data');
  const value = 'value';// 必要なものだけ抽出
  
  return {
    getValue: () => value// largeArrayは参照されないのでGC可能
  };
}

3.2 バッファの効率的な使用

JavaScript
// 悪い例: 文字列連結(遅い)
function concatStrings(count) {
  let result = '';
  for (let i = 0; i < count; i++) {
    result += 'a';// 毎回新しい文字列が作成される
  }
  return result;
}

// 良い例: 配列でバッファリング(速い)
function concatStringsOptimized(count) {
  const arr = [];
  for (let i = 0; i < count; i++) {
    arr.push('a');
  }
  return arr.join('');
}

// Buffer の使用
function processLargeData() {
  // 悪い例: 文字列として扱う
  const data = 'x'.repeat(1000000);
  // 処理...
  
  // 良い例: Bufferとして扱う
  const buffer = Buffer.alloc(1000000);
  buffer.fill('x');
  // 処理...
}

// Buffer プールの活用
const bufferPool = Buffer.allocUnsafe(1024 * 1024);// 1MB
let offset = 0;

function allocateFromPool(size) {
  if (offset + size > bufferPool.length) {
    offset = 0;// リセット
  }
  
  const slice = bufferPool.slice(offset, offset + size);
  offset += size;
  return slice;
}

3.3 ガベージコレクションの監視

JavaScript
// GCイベントの監視(要 --expose-gc フラグ)
const v8 = require('v8');

// ヒープ統計の取得
function printHeapStats() {
  const heapStats = v8.getHeapStatistics();
  
  console.log('=== ヒープ統計 ===');
  console.log(`総ヒープサイズ: ${(heapStats.total_heap_size / 1024 / 1024).toFixed(2)} MB`);
  console.log(`使用中: ${(heapStats.used_heap_size / 1024 / 1024).toFixed(2)} MB`);
  console.log(`制限: ${(heapStats.heap_size_limit / 1024 / 1024).toFixed(2)} MB`);
  console.log(`物理的な使用量: ${(heapStats.total_physical_size / 1024 / 1024).toFixed(2)} MB`);
}

// GCの強制実

if (global.gc) {
  console.log('GC実行前:');
  printHeapStats();
  
  global.gc();
  
  console.log('\nGC実行後:');
  printHeapStats();
}

// ヒープスナップショットの取得
const fs = require('fs');

function takeHeapSnapshot() {
  const snapshot = v8.writeHeapSnapshot();
  console.log(`ヒープスナップショット保存: ${snapshot}`);
  // Chrome DevToolsで開いて分析可能
}

4. クラスタリングによるマルチコア活用

4.1 基本的なクラスタリング

Node.jsはシングルスレッドですが、clusterモジュールで複数プロセスを起動できます。

JavaScript
const cluster = require('cluster');
const http = require('http');
const os = require('os');

const numCPUs = os.cpus().length;

if (cluster.isMaster) {
  console.log(`マスタープロセス ${process.pid} が起動`);
  console.log(`${numCPUs}個のワーカーを起動します`);
  
  // ワーカープロセスを起動
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  
  // ワーカーが終了した場合、再起動
  cluster.on('exit', (worker, code, signal) => {
    console.log(`ワーカー ${worker.process.pid} が終了しました`);
    console.log('新しいワーカーを起動します');
    cluster.fork();
  });
  
} else {
  // ワーカープロセス
  const server = http.createServer((req, res) => {
    // 重い処理をシミュレート
    let sum = 0;
    for (let i = 0; i < 1000000; i++) {
      sum += Math.sqrt(i);
    }
    
    res.writeHead(200);
    res.end(`ワーカー ${process.pid} が処理しました\n結果: ${sum}`);
  });
  
  server.listen(3000, () => {
    console.log(`ワーカー ${process.pid} がポート3000で待機中`);
  });
}

4.2 ラウンドロビン負荷分散

JavaScript
const cluster = require('cluster');
const http = require('http');
const os = require('os');

if (cluster.isMaster) {
  const numCPUs = os.cpus().length;
  const workers = [];
  
  console.log(`マスタープロセス起動: PID ${process.pid}`);
  
  // ワーカーを起動
  for (let i = 0; i < numCPUs; i++) {
    const worker = cluster.fork();
    workers.push(worker);
  }
  
  // 統計情報を収集
  const stats = {};
  workers.forEach(worker => {
    stats[worker.process.pid] = 0;
  });
  
  // ワーカーからのメッセージを受信
  Object.values(cluster.workers).forEach(worker => {
    worker.on('message', (msg) => {
      if (msg.cmd === 'notifyRequest') {
        stats[worker.process.pid]++;
      }
    });
  });
  
  // 定期的に統計を表示
  setInterval(() => {
    console.log('\n=== リクエスト統計 ===');
    Object.entries(stats).forEach(([pid, count]) => {
      console.log(`ワーカー ${pid}: ${count}リクエスト`);
    });
  }, 5000);
  
  cluster.on('exit', (worker) => {
    console.log(`ワーカー ${worker.process.pid} が終了`);
    delete stats[worker.process.pid];
    const newWorker = cluster.fork();
    stats[newWorker.process.pid] = 0;
  });
  
} else {
  const server = http.createServer((req, res) => {
    // マスタープロセスに通知
    process.send({ cmd: 'notifyRequest' });
    
    res.writeHead(200);
    res.end(`処理完了 (PID: ${process.pid})\n`);
  });
  
  server.listen(3000);
}

4.3 グレースフルリスタート

ダウンタイムなしでワーカーを再起動する方法です。

JavaScript
const cluster = require('cluster');
const http = require('http');
const os = require('os');

if (cluster.isMaster) {
  const workers = [];
  const numCPUs = os.cpus().length;
  
  // ワーカーを起動
  for (let i = 0; i < numCPUs; i++) {
    createWorker();
  }
  
  function createWorker() {
    const worker = cluster.fork();
    console.log(`ワーカー ${worker.process.pid} を起動`);
    
    worker.on('message', (msg) => {
      if (msg === 'shutdown') {
        worker.disconnect();
      }
    });
    
    return worker;
  }
  
  // グレースフルリスタート
  function gracefulRestart() {
    const workerIds = Object.keys(cluster.workers);
    
    function restartWorker(i) {
      if (i >= workerIds.length) {
        console.log('すべてのワーカーを再起動しました');
        return;
      }
      
      const worker = cluster.workers[workerIds[i]];
      
      console.log(`ワーカー ${worker.process.pid} を再起動中...`);
      
      // 新しいワーカーを起動
      const newWorker = createWorker();
      
      // 新しいワーカーが準備できたら古いワーカーを停止
      newWorker.on('listening', () => {
        worker.send('shutdown');
        
        setTimeout(() => {
          restartWorker(i + 1);
        }, 1000);
      });
    }
    
    restartWorker(0);
  }
  
  // SIGUSRシグナルでリスタート
  process.on('SIGUSR2', () => {
    console.log('グレースフルリスタートを開始...');
    gracefulRestart();
  });
  
} else {
  const server = http.createServer((req, res) => {
    res.end(`PID: ${process.pid}\n`);
  });
  
  server.listen(3000);
  
  process.on('message', (msg) => {
    if (msg === 'shutdown') {
      server.close(() => {
        process.exit(0);
      });
    }
  });
}

// リスタート方法: kill -USR2 <master_pid>

5. Worker Threadsによる並列処理

5.1 基本的なWorker Threads

CPU集約的な処理を別スレッドで実行します。

JavaScript
// main.js
const { Worker } = require('worker_threads');

function runWorker(workerData) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js', { workerData });
    
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0) {
        reject(new Error(`ワーカーがコード${code}で終了しました`));
      }
    });
  });
}

// 使用例
(async () => {
  console.log('重い計算を開始...');
  
  const result = await runWorker({ n: 45 });
  
  console.log('結果:', result);
})();

// worker.js
const { parentPort, workerData } = require('worker_threads');

// フィボナッチ数列(重い計算)
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

const result = fibonacci(workerData.n);
parentPort.postMessage(result);

5.2 Worker Poolの実装

複数のWorkerを効率的に管理します。

JavaScript
const { Worker } = require('worker_threads');
const os = require('os');

class WorkerPool {
  constructor(workerScript, poolSize = os.cpus().length) {
    this.workerScript = workerScript;
    this.poolSize = poolSize;
    this.workers = [];
    this.freeWorkers = [];
    this.queue = [];
    
    // ワーカーを初期化
    for (let i = 0; i < poolSize; i++) {
      this.addWorker();
    }
  }

  addWorker() {
    const worker = new Worker(this.workerScript);
    
    worker.on('message', (result) => {
      const { resolve } = worker.currentTask;
      resolve(result);
      this.freeWorkers.push(worker);
      this.runNext();
    });
    
    worker.on('error', (err) => {
      const { reject } = worker.currentTask;
      reject(err);
      this.freeWorkers.push(worker);
      this.runNext();
    });
    
    this.workers.push(worker);
    this.freeWorkers.push(worker);
  }

  runNext() {
    if (this.queue.length === 0) return;
    if (this.freeWorkers.length === 0) return;
    
    const { workerData, resolve, reject } = this.queue.shift();
    const worker = this.freeWorkers.pop();
    
    worker.currentTask = { resolve, reject };
    worker.postMessage(workerData);
  }

  exec(workerData) {
    return new Promise((resolve, reject) => {
      this.queue.push({ workerData, resolve, reject });
      this.runNext();
    });
  }

  destroy() {
    this.workers.forEach(worker => worker.terminate());
  }
}

// 使用例
const pool = new WorkerPool('./heavy-calculation-worker.js', 4);

async function processData() {
  const tasks = Array.from({ length: 20 }, (_, i) => 
    pool.exec({ n: 40 + i })
  );
  
  const results = await Promise.all(tasks);
  console.log('すべての計算完了:', results);
  
  pool.destroy();
}

processData();

5.3 SharedArrayBufferによるデータ共有

複数のWorker間でメモリを共有します。

JavaScript
// main.js
const { Worker } = require('worker_threads');

// 共有メモリを作成
const sharedBuffer = new SharedArrayBuffer(1024);
const sharedArray = new Int32Array(sharedBuffer);

// 初期値を設定
for (let i = 0; i < sharedArray.length; i++) {
  sharedArray[i] = i;
}

// 複数のWorkerで処理
const workers = [];
for (let i = 0; i < 4; i++) {
  const worker = new Worker('./shared-worker.js', {
    workerData: { sharedBuffer, workerId: i }
  });
  
  worker.on('message', (msg) => {
    console.log(`ワーカー${i}: ${msg}`);
  });
  
  workers.push(worker);
}

// shared-worker.js
const { parentPort, workerData } = require('worker_threads');
const { sharedBuffer, workerId } = workerData;

const sharedArray = new Int32Array(sharedBuffer);

// 各Workerが配列の一部を処理
const chunkSize = Math.floor(sharedArray.length / 4);
const start = workerId * chunkSize;
const end = start + chunkSize;

for (let i = start; i < end; i++) {
  // Atomics を使って安全に更新
  Atomics.add(sharedArray, i, 100);
}

parentPort.postMessage(`処理完了: ${start}${end}`);

6. キャッシング戦略

6.1 メモリキャッシュの実装

JavaScript
class MemoryCache {
  constructor(options = {}) {
    this.cache = new Map();
    this.maxSize = options.maxSize || 1000;
    this.ttl = options.ttl || 60000; <em>// デフォルト60秒</em>
  }

  set(key, value, ttl = this.ttl) {
    // サイズ制限チェック
    if (this.cache.size >= this.maxSize) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    
    this.cache.set(key, {
      value,
      expires: Date.now() + ttl
    });
  }

  get(key) {
    const item = this.cache.get(key);
    
    if (!item) return null;
    
    // 有効期限チェック
    if (Date.now() > item.expires) {
      this.cache.delete(key);
      return null;
    }
    
    return item.value;
  }

  has(key) {
    return this.get(key) !== null;
  }

  delete(key) {
    return this.cache.delete(key);
  }

  clear() {
    this.cache.clear();
  }

  // 有効期限切れのエントリを削除
  cleanup() {
    const now = Date.now();
    for (const [key, item] of this.cache.entries()) {
      if (now > item.expires) {
        this.cache.delete(key);
      }
    }
  }
}

// 使用例
const cache = new MemoryCache({ maxSize: 100, ttl: 5000 });

// データをキャッシュ
cache.set('user:1', { id: 1, name: 'Alice' });
cache.set('user:2', { id: 2, name: 'Bob' }, 10000); <em>// カスタムTTL</em>

// データを取得
console.log(cache.get('user:1')); <em>// => { id: 1, name: 'Alice' }</em>

// 定期的にクリーンアップ
setInterval(() => cache.cleanup(), 60000);

6.2 関数メモ化

計算結果をキャッシュして、同じ入力に対する再計算を避けます。

JavaScript
// シンプルなメモ化
function memoize(fn) {
  const cache = new Map();
  
  return function(...args) {
    const key = JSON.stringify(args);
    
    if (cache.has(key)) {
      console.log('キャッシュヒット:', key);
      return cache.get(key);
    }
    
    console.log('計算実行:', key);
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// 使用例: フィボナッチ数列
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

const memoizedFib = memoize(fibonacci);

console.time('1回目');
console.log(memoizedFib(35));// 計算実行
console.timeEnd('1回目');

console.time('2回目');
console.log(memoizedFib(35));// キャッシュから取得(超高速)
console.timeEnd('2回目');

// TTL付きメモ化
function memoizeWithTTL(fn, ttl = 60000) {
  const cache = new Map();
  
  return function(...args) {
    const key = JSON.stringify(args);
    const cached = cache.get(key);
    
    if (cached && Date.now() < cached.expires) {
      return cached.value;
    }
    
    const result = fn.apply(this, args);
    cache.set(key, {
      value: result,
      expires: Date.now() + ttl
    });
    
    return result;
  };
}

// APIコールをメモ化(5秒間キャッシュ)
const fetchUserMemoized = memoizeWithTTL(async (userId) => {
  console.log('APIコール:', userId);
  const response = await fetch(`https://api.example.com/users/${userId}`);
  return response.json();
}, 5000);

6.3 HTTPキャッシュヘッダー

適切なキャッシュヘッダーを設定して、クライアント側でキャッシュを活用します。

JavaScript
const http = require('http');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

function generateETag(content) {
  return crypto.createHash('md5').update(content).digest('hex');
}

const server = http.createServer((req, res) => {
  const filePath = path.join(__dirname, 'public', req.url);
  
  fs.readFile(filePath, (err, content) => {
    if (err) {
      res.writeHead(404);
      res.end('Not Found');
      return;
    }
    
    <em>// ETagを生成</em>
    const etag = generateETag(content);
    
    <em>// クライアントからのETagと比較</em>
    if (req.headers['if-none-match'] === etag) {
      res.writeHead(304); <em>// Not Modified</em>
      res.end();
      return;
    }
    
    <em>// キャッシュヘッダーを設定</em>
    res.writeHead(200, {
      'Content-Type': 'text/html',
      'Cache-Control': 'public, max-age=3600', <em>// 1時間キャッシュ</em>
      'ETag': etag,
      'Last-Modified': fs.statSync(filePath).mtime.toUTCString()
    });
    
    res.end(content);
  });
});

server.listen(3000);

7. データベースクエリの最適化

7.1 コネクションプール

データベース接続を再利用して、オーバーヘッドを削減します。

JavaScript
// 簡易的なコネクションプールの実装
class ConnectionPool {
  constructor(createConnection, options = {}) {
    this.createConnection = createConnection;
    this.maxSize = options.maxSize || 10;
    this.minSize = options.minSize || 2;
    this.pool = [];
    this.inUse = new Set();
    
    // 最小接続数を作成
    for (let i = 0; i < this.minSize; i++) {
      this.pool.push(this.createConnection());
    }
  }

  async acquire() {
    // プールに空きがあれば取得
    if (this.pool.length > 0) {
      const conn = this.pool.pop();
      this.inUse.add(conn);
      return conn;
    }
    
    // 最大数に達していなければ新規作成
    if (this.inUse.size < this.maxSize) {
      const conn = await this.createConnection();
      this.inUse.add(conn);
      return conn;
    }
    
    // 空きが出るまで待機
    return new Promise((resolve) => {
      const checkPool = setInterval(() => {
        if (this.pool.length > 0) {
          clearInterval(checkPool);
          const conn = this.pool.pop();
          this.inUse.add(conn);
          resolve(conn);
        }
      }, 100);
    });
  }

  release(conn) {
    this.inUse.delete(conn);
    this.pool.push(conn);
  }

  async query(sql, params) {
    const conn = await this.acquire();
    try {
      return await conn.query(sql, params);
    } finally {
      this.release(conn);
    }
  }

  async close() {
    // すべての接続を閉じる
    for (const conn of this.pool) {
      await conn.close();
    }
    for (const conn of this.inUse) {
      await conn.close();
    }
  }
}

// 使用例(疑似コード)
const pool = new ConnectionPool(
  () => createDatabaseConnection({ host: 'localhost', database: 'mydb' }),
  { maxSize: 20, minSize: 5 }
);

async function getUsers() {
  const result = await pool.query('SELECT * FROM users WHERE active = ?', [true]);
  return result.rows;
}

7.2 バッチ処理

複数のクエリをまとめて実行します。

JavaScript
// 悪い例: N+1問題
async function getUsersWithOrders() {
  const users = await db.query('SELECT * FROM users');
  
  for (const user of users) {
    // ユーザーごとにクエリ実行(遅い!)
    user.orders = await db.query('SELECT * FROM orders WHERE user_id = ?', [user.id]);
  }
  
  return users;
}

// 良い例: JOIN を使用
async function getUsersWithOrdersOptimized() {
  const result = await db.query(`
    SELECT 
      u.*,
      o.id as order_id,
      o.amount,
      o.created_at as order_date
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
  `);
  
  // 結果をグループ化
  const usersMap = new Map();
  
  for (const row of result.rows) {
    if (!usersMap.has(row.id)) {
      usersMap.set(row.id, {
        id: row.id,
        name: row.name,
        email: row.email,
        orders: []
      });
    }
    
    if (row.order_id) {
      usersMap.get(row.id).orders.push({
        id: row.order_id,
        amount: row.amount,
        date: row.order_date
      });
    }
  }
  
  return Array.from(usersMap.values());
}

// バルクインサート
async function insertManyUsers(users) {
  // 悪い例: 1件ずつインサート
  // for (const user of users) {
  //   await db.query('INSERT INTO users (name, email) VALUES (?, ?)', [user.name, user.email]);
  // }
  
  // 良い例: バルクインサート
  const values = users.map(u => `('${u.name}', '${u.email}')`).join(',');
  await db.query(`INSERT INTO users (name, email) VALUES ${values}`);
}

7.3 クエリキャッシュ

頻繁に実行されるクエリの結果をキャッシュします。

JavaScript
class QueryCache {
  constructor(ttl = 60000) {
    this.cache = new Map();
    this.ttl = ttl;
  }

  generateKey(sql, params) {
    return `${sql}:${JSON.stringify(params)}`;
  }

  async query(db, sql, params, ttl = this.ttl) {
    const key = this.generateKey(sql, params);
    const cached = this.cache.get(key);
    
    if (cached && Date.now() < cached.expires) {
      console.log('キャッシュヒット:', key);
      return cached.result;
    }
    
    console.log('DBクエリ実行:', key);
    const result = await db.query(sql, params);
    
    this.cache.set(key, {
      result,
      expires: Date.now() + ttl
    });
    
    return result;
  }

  invalidate(pattern) {
    // パターンに一致するキャッシュを削除
    for (const key of this.cache.keys()) {
      if (key.includes(pattern)) {
        this.cache.delete(key);
      }
    }
  }

  clear() {
    this.cache.clear();
  }
}

// 使用例
const queryCache = new QueryCache(30000);// 30秒キャッシュ

async function getActiveUsers() {
  return queryCache.query(
    db,
    'SELECT * FROM users WHERE active = ?',
    [true],
    60000// このクエリは60秒キャッシュ
  );
}

// ユーザー更新時にキャッシュを無効化
async function updateUser(userId, data) {
  await db.query('UPDATE users SET ? WHERE id = ?', [data, userId]);
  queryCache.invalidate('users');// users関連のキャッシュをクリア
}

8. プロファイリングとデバッグ

8.1 組み込みプロファイラー

Node.jsの組み込みプロファイラーを使用します。

JavaScript
// プロファイリングを有効にして起動
// node --prof app.js

// 処理を実行
const crypto = require('crypto');

function heavyComputation() {
  const arr = [];
  for (let i = 0; i < 1000000; i++) {
    arr.push(crypto.randomBytes(16).toString('hex'));
  }
  return arr;
}

console.time('処理時間');
heavyComputation();
console.timeEnd('処理時間');

// 終了後、isolate-*-v8.log ファイルが生成される
// 解析: node --prof-process isolate-*-v8.log > processed.txt

8.2 パフォーマンスフックの使用

JavaScript
const { PerformanceObserver, performance } = require('perf_hooks');

// パフォーマンス測定を監視
const obs = new PerformanceObserver((items) => {
  items.getEntries().forEach((entry) => {
    console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
  });
});

obs.observe({ entryTypes: ['measure'] });

// 測定開始
performance.mark('start-operation');

// 何か処理
setTimeout(() => {
  performance.mark('end-operation');
  
  // 測定終了
  performance.measure(
    'operation-duration',
    'start-operation',
    'end-operation'
  );
}, 1000);

// 複数の処理を測定
async function measureDatabaseOperations() {
  performance.mark('db-start');
  
  // クエリ1
  performance.mark('query1-start');
  await db.query('SELECT * FROM users');
  performance.mark('query1-end');
  performance.measure('Query 1', 'query1-start', 'query1-end');
  
  // クエリ2
  performance.mark('query2-start');
  await db.query('SELECT * FROM orders');
  performance.mark('query2-end');
  performance.measure('Query 2', 'query2-start', 'query2-end');
  
  performance.mark('db-end');
  performance.measure('Total DB Time', 'db-start', 'db-end');
}

8.3 メモリリークの検出

JavaScript
const { performance } = require('perf_hooks');

class MemoryMonitor {
  constructor(interval = 5000) {
    this.interval = interval;
    this.measurements = [];
    this.timer = null;
  }

  start() {
    this.timer = setInterval(() => {
      const mem = process.memoryUsage();
      const measurement = {
        timestamp: Date.now(),
        heapUsed: mem.heapUsed,
        heapTotal: mem.heapTotal,
        external: mem.external,
        rss: mem.rss
      };
      
      this.measurements.push(measurement);
      
      // 増加傾向を検出
      if (this.measurements.length >= 10) {
        this.detectLeak();
      }
    }, this.interval);
  }

  stop() {
    if (this.timer) {
      clearInterval(this.timer);
    }
  }

  detectLeak() {
    const recent = this.measurements.slice(-10);
    const first = recent[0];
    const last = recent[recent.length - 1];
    
    const heapGrowth = last.heapUsed - first.heapUsed;
    const timespan = last.timestamp - first.timestamp;
    const growthRate = heapGrowth / timespan; // バイト/ms
    
    if (growthRate > 1000) { // 1秒あたり1MB以上の増加
      console.warn('⚠️  メモリリークの可能性があります');
      console.warn(`増加率: ${(growthRate * 1000 / 1024 / 1024).toFixed(2)} MB/秒`);
      console.warn(`現在のヒープ: ${(last.heapUsed / 1024 / 1024).toFixed(2)} MB`);
    }
  }

  report() {
    if (this.measurements.length === 0) return;
    
    const latest = this.measurements[this.measurements.length - 1];
    console.log('\n=== メモリ使用状況 ===');
    console.log(`ヒープ使用量: ${(latest.heapUsed / 1024 / 1024).toFixed(2)} MB`);
    console.log(`ヒープ合計: ${(latest.heapTotal / 1024 / 1024).toFixed(2)} MB`);
    console.log(`外部メモリ: ${(latest.external / 1024 / 1024).toFixed(2)} MB`);
    console.log(`RSS: ${(latest.rss / 1024 / 1024).toFixed(2)} MB`);
  }
}

// 使用例
const monitor = new MemoryMonitor(5000);
monitor.start();

// 定期的にレポート
setInterval(() => {
  monitor.report();
}, 30000);

// プロセス終了時
process.on('SIGINT', () => {
  monitor.stop();
  monitor.report();
  process.exit(0);
});

8.4 CPUプロファイリング

JavaScript
const v8Profiler = require('v8-profiler-next');
const fs = require('fs');

// CPUプロファイリングを開始
const title = 'cpu-profile';
v8Profiler.startProfiling(title, true);

// 重い処理を実行
function heavyOperation() {
  let sum = 0;
  for (let i = 0; i < 10000000; i++) {
    sum += Math.sqrt(i);
  }
  return sum;
}

setTimeout(() => {
  heavyOperation();
  
  // プロファイリングを停止
  const profile = v8Profiler.stopProfiling(title);
  
  // ファイルに保存
  profile.export((error, result) => {
    fs.writeFileSync(`${title}.cpuprofile`, result);
    profile.delete();
    console.log('CPUプロファイルを保存しました');
    console.log('Chrome DevToolsで開いて分析してください');
  });
}, 1000);

// ヒープスナップショットの取得
function takeHeapSnapshot(name) {
  const snapshot = v8Profiler.takeSnapshot(name);
  
  snapshot.export((error, result) => {
    fs.writeFileSync(`${name}.heapsnapshot`, result);
    snapshot.delete();
    console.log('ヒープスナップショットを保存しました');
  });
}

// 定期的にスナップショット取得
setInterval(() => {
  takeHeapSnapshot(`heap-${Date.now()}`);
}, 60000);

実践例:高性能なAPIサーバー

これまでの技術を統合した、最適化されたAPIサーバーの例です。

JavaScript
const cluster = require('cluster');
const http = require('http');
const os = require('os');
const { URL } = require('url');

// キャッシュクラス
class Cache {
  constructor(ttl = 60000) {
    this.cache = new Map();
    this.ttl = ttl;
    
    // 定期クリーンアップ
    setInterval(() => this.cleanup(), ttl);
  }

  set(key, value, ttl = this.ttl) {
    this.cache.set(key, {
      value,
      expires: Date.now() + ttl
    });
  }

  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;
    if (Date.now() > item.expires) {
      this.cache.delete(key);
      return null;
    }
    return item.value;
  }

  cleanup() {
    const now = Date.now();
    for (const [key, item] of this.cache.entries()) {
      if (now > item.expires) {
        this.cache.delete(key);
      }
    }
  }
}

if (cluster.isMaster) {
  const numCPUs = os.cpus().length;
  console.log(`マスタープロセス ${process.pid} 起動`);
  console.log(`${numCPUs}個のワーカーを起動します`);
  
  // ワーカーを起動
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  
  // ワーカーの再起動
  cluster.on('exit', (worker, code, signal) => {
    console.log(`ワーカー ${worker.process.pid} が終了しました`);
    cluster.fork();
  });
  
  // 統計情報
  const stats = { requests: 0, errors: 0 };
  
  Object.values(cluster.workers).forEach(worker => {
    worker.on('message', (msg) => {
      if (msg.type === 'stats') {
        stats.requests += msg.requests;
        stats.errors += msg.errors;
      }
    });
  });
  
  // 定期レポート
  setInterval(() => {
    console.log(`\n総リクエスト数: ${stats.requests}`);
    console.log(`総エラー数: ${stats.errors}`);
  }, 30000);
  
} else {
  // ワーカープロセス
  const cache = new Cache(30000);// 30秒キャッシュ
  let requestCount = 0;
  let errorCount = 0;
  
  // データ取得関数(キャッシュ付き)
  async function getData(id) {
    const cacheKey = `data:${id}`;
    const cached = cache.get(cacheKey);
    
    if (cached) {
      return cached;
    }
    
    // データベースから取得(シミュレーション)
    await new Promise(resolve => setTimeout(resolve, 50));
    const data = { id, value: Math.random(), timestamp: Date.now() };
    
    cache.set(cacheKey, data);
    return data;
  }
  
  // リクエストハンドラ
  const server = http.createServer(async (req, res) => {
    requestCount++;
    
    try {
      const url = new URL(req.url, `http://${req.headers.host}`);
      
      if (url.pathname === '/api/data' && req.method === 'GET') {
        const id = url.searchParams.get('id') || '1';
        const data = await getData(id);
        
        res.writeHead(200, {
          'Content-Type': 'application/json',
          'Cache-Control': 'public, max-age=30'
        });
        res.end(JSON.stringify({ success: true, data }));
        
      } else if (url.pathname === '/api/health' && req.method === 'GET') {
        const memUsage = process.memoryUsage();
        
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
          success: true,
          pid: process.pid,
          uptime: process.uptime(),
          memory: {
            heapUsed: `${(memUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`,
            heapTotal: `${(memUsage.heapTotal / 1024 / 1024).toFixed(2)} MB`
          }
        }));
        
      } else {
        res.writeHead(404, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ success: false, error: 'Not Found' }));
      }
      
    } catch (err) {
      errorCount++;
      console.error('エラー:', err);
      res.writeHead(500, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ success: false, error: 'Internal Server Error' }));
    }
  });
  
  server.listen(3000, () => {
    console.log(`ワーカー ${process.pid} がポート3000で待機中`);
  });
  
  // 定期的に統計をマスタープロセスに送信
  setInterval(() => {
    process.send({
      type: 'stats',
      requests: requestCount,
      errors: errorCount
    });
  }, 10000);
}

まとめ

Node.jsのパフォーマンス最適化は、適切な測定と戦略的な改善が鍵となります。

重要なポイント:

測定と分析

  • ✅ 最適化前に必ずベンチマークを取る
  • ✅ プロファイラーでボトルネックを特定
  • ✅ メモリ使用量を定期的に監視

非同期処理

  • ✅ 並列実行可能な処理はPromise.all()で
  • ✅ 大量の並列処理は制限をかける
  • ✅ setImmediateで重い処理を分割

メモリ管理

  • ✅ メモリリークを防ぐ(イベントリスナー削除など)
  • ✅ バッファを効率的に使用
  • ✅ LRUキャッシュでメモリを制限

マルチコア活用

  • ✅ clusterでCPUコアを最大限活用
  • ✅ Worker ThreadsでCPU集約的な処理を分離
  • ✅ グレースフルリスタートで無停止デプロイ

キャッシング

  • ✅ 頻繁にアクセスされるデータをキャッシュ
  • ✅ 適切なTTLを設定
  • ✅ HTTPキャッシュヘッダーを活用

データベース

  • ✅ コネクションプールを使用
  • ✅ N+1問題を避ける
  • ✅ クエリをバッチ処理

これでNode.js実践入門シリーズは完結です。

これらの技術を組み合わせることで、高性能で信頼性の高いNode.jsアプリケーションを構築できます!

シリーズ記事
第1回: Node.jsとは?環境構築の実践-インストールからプロジェクト作成まで
第2回:コアモジュール基礎:ファイル操作とWebサーバー
第3回: イベント駆動プログラミング
第4回: ストリーム処理をマスターする
第5回: 堅牢なエラーハンドリング
第6回: パフォーマンス最適化テクニック ← 今回