Node.jsアプリケーションのパフォーマンスを最大限に引き出すための実践的なテクニックを学びます。
メモリ管理、並列処理、キャッシング、プロファイリングなど、本番環境で役立つ知識を網羅します。
💡この記事でわかる事
以下の内容を習得することができます:
- パフォーマンス測定の基礎
- 非同期処理の最適化
- メモリ管理とガベージコレクション
- クラスタリングによるマルチコア活用
- Worker Threadsによる並列処理
- キャッシング戦略
- データベースクエリの最適化
- プロファイリングとデバッグ
Contents
1. パフォーマンス測定の基礎
最適化の前に、まず現状を測定することが重要です。
1.1 実行時間の測定
Node.jsには複数の時間測定方法があります。
// 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 ベンチマークの実装
複数の実装を比較するベンチマークツールです。
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 メモリ使用量の測定
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.js2. 非同期処理の最適化
2.1 Promise.all() vs 逐次実行
並列実行可能な処理は、Promise.all()で高速化できます。
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 並列実行数の制限
大量のリクエストを送る場合、並列数を制限することでリソースを節約できます。
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
イベントループの仕組みを理解して、適切に使い分けます。
// 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 メモリリークの検出
よくあるメモリリークのパターンとその対策です。
// 悪い例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 バッファの効率的な使用
// 悪い例: 文字列連結(遅い)
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 ガベージコレクションの監視
// 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モジュールで複数プロセスを起動できます。
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 ラウンドロビン負荷分散
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 グレースフルリスタート
ダウンタイムなしでワーカーを再起動する方法です。
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集約的な処理を別スレッドで実行します。
// 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を効率的に管理します。
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();複数のWorker間でメモリを共有します。
// 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 メモリキャッシュの実装
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 関数メモ化
計算結果をキャッシュして、同じ入力に対する再計算を避けます。
// シンプルなメモ化
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キャッシュヘッダー
適切なキャッシュヘッダーを設定して、クライアント側でキャッシュを活用します。
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 コネクションプール
データベース接続を再利用して、オーバーヘッドを削減します。
// 簡易的なコネクションプールの実装
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 バッチ処理
複数のクエリをまとめて実行します。
// 悪い例: 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 クエリキャッシュ
頻繁に実行されるクエリの結果をキャッシュします。
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の組み込みプロファイラーを使用します。
// プロファイリングを有効にして起動
// 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.txt8.2 パフォーマンスフックの使用
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 メモリリークの検出
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プロファイリング
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サーバーの例です。
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回: パフォーマンス最適化テクニック ← 今回


























