Node.jsパフォーマンス最適化の実践 - レスポンス時間を大幅改善する5つの手法

に公開

はじめに

Node.jsアプリケーションが遅い、サーバーコストが高い、ユーザーから「重い」と言われる...このような課題に直面していませんか?

この記事では、実務で効果が期待できる5つのパフォーマンス最適化手法を紹介します。各手法はベストプラクティスに基づいており、適切に実装することで大幅なパフォーマンス改善が見込めます。

1. イベントループのブロッキング回避

問題点

Node.jsはシングルスレッドのイベントループで動作するため、CPU集約的な処理がイベントループをブロックすると、全てのリクエストが遅延します。

解決策: Worker Threadsの活用

import { Worker } from 'worker_threads';

// 重い計算処理をWorker Threadsで実行
function runHeavyCalculation(data: number[]): Promise<number> {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./calculation-worker.js', {
      workerData: data
    });

    worker.on('message', resolve);
    worker.on('error', reject);
  });
}

// APIハンドラー
app.post('/calculate', async (req, res) => {
  const result = await runHeavyCalculation(req.body.data);
  res.json({ result });
});

期待される効果:

  • CPU集約的な処理をメインスレッドから分離
  • 同時リクエスト処理能力の大幅な向上
  • レスポンスタイムの安定化

2. 効率的なストリーム処理

問題点

大きなファイルやデータを一度にメモリに読み込むと、メモリ不足やガベージコレクションの頻発を引き起こします。

解決策: Streamの適切な利用

import { pipeline } from 'stream/promises';
import { createReadStream, createWriteStream } from 'fs';
import { createGzip } from 'zlib';

// ❌ 非効率: 全データをメモリに読み込む
async function processFileBad(inputPath: string, outputPath: string) {
  const data = await fs.readFile(inputPath);
  const compressed = await gzip(data);
  await fs.writeFile(outputPath, compressed);
}

// ✅ 効率的: ストリーム処理
async function processFileGood(inputPath: string, outputPath: string) {
  await pipeline(
    createReadStream(inputPath),
    createGzip(),
    createWriteStream(outputPath)
  );
}

期待される効果:

  • メモリ使用量の劇的な削減
  • 大容量ファイル処理のスループット向上
  • ガベージコレクションの頻度が大幅に減少

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

問題点

N+1クエリ問題や不適切なインデックス設計は、データベースアクセスのボトルネックになります。

解決策: リレーションの事前ロードとインデックス最適化

// ❌ N+1問題が発生
async function getUsersWithPostsBad() {
  const users = await prisma.user.findMany();

  for (const user of users) {
    user.posts = await prisma.post.findMany({
      where: { userId: user.id }
    });
  }

  return users;
}

// ✅ 事前ロードで1回のクエリに最適化
async function getUsersWithPostsGood() {
  return await prisma.user.findMany({
    include: {
      posts: {
        select: {
          id: true,
          title: true,
          createdAt: true
        }
      }
    }
  });
}

// ✅ 適切なインデックス設計(Prisma Schema)
model Post {
  id        Int      @id @default(autoincrement())
  userId    Int
  title     String
  createdAt DateTime @default(now())

  user User @relation(fields: [userId], references: [id])

  @@index([userId])           // 外部キーにインデックス
  @@index([createdAt])        // 検索条件にインデックス
  @@index([userId, createdAt]) // 複合インデックス
}

期待される効果:

  • クエリ実行時間の大幅な短縮
  • データベース負荷の軽減
  • 複雑なクエリのレスポンスタイムが安定

4. 適切なキャッシング戦略

問題点

同じデータを繰り返し計算・取得することは、無駄なリソース消費につながります。

解決策: 多層キャッシング戦略

import { Redis } from 'ioredis';
import NodeCache from 'node-cache';

const redis = new Redis();
const memoryCache = new NodeCache({ stdTTL: 60 });

// 多層キャッシング
async function getUserData(userId: string) {
  // L1: メモリキャッシュ(最速)
  const cachedInMemory = memoryCache.get(userId);
  if (cachedInMemory) {
    return cachedInMemory;
  }

  // L2: Redisキャッシュ
  const cachedInRedis = await redis.get(`user:${userId}`);
  if (cachedInRedis) {
    const data = JSON.parse(cachedInRedis);
    memoryCache.set(userId, data);
    return data;
  }

  // L3: データベース
  const user = await prisma.user.findUnique({
    where: { id: userId }
  });

  // キャッシュに保存
  await redis.setex(`user:${userId}`, 300, JSON.stringify(user));
  memoryCache.set(userId, user);

  return user;
}

期待される効果:

  • 頻繁にアクセスされるデータの取得時間が劇的に短縮
  • データベース負荷の大幅な軽減
  • レスポンスタイムのばらつきが減少

5. コネクションプールの最適化

問題点

データベースコネクションの確立は高コストな処理です。適切なプール設定がないと、パフォーマンスが低下します。

解決策: 負荷に応じたプール設定

// ❌ デフォルト設定のまま
const prisma = new PrismaClient();

// ✅ 負荷に応じた最適化
const prisma = new PrismaClient({
  datasources: {
    db: {
      url: process.env.DATABASE_URL,
    },
  },
  // コネクションプールの設定
  // URL例: postgresql://user:pass@host:5432/db?connection_limit=20&pool_timeout=10
});

// 推奨設定値(想定同時接続数に応じて調整)
// - 小規模(~100 req/sec): connection_limit=10
// - 中規模(~500 req/sec): connection_limit=20
// - 大規模(1000+ req/sec): connection_limit=50

期待される効果:

  • コネクション待ち時間の短縮
  • スパイクトラフィックへの耐性が向上
  • データベースサーバーの負荷が安定化

まとめ

この記事では、Node.jsアプリケーションのパフォーマンスを向上させる5つの手法を紹介しました。

手法 期待される効果 実装難易度
Worker Threads CPU集約処理の大幅高速化
ストリーム処理 メモリ使用量の劇的削減
DBクエリ最適化 クエリ実行時間の大幅短縮
キャッシング データ取得時間の劇的短縮
コネクションプール 接続待ち時間の改善

これらの手法を組み合わせることで、アプリケーションの特性に応じて大幅なパフォーマンス改善が期待できます。

より深く学びたい方へ

この記事では、各手法の基本的な実装を紹介しましたが、実務では以下のような追加の考慮事項があります:

  • フレームワーク別(Express/NestJS/Fastify)の最適化パターン
  • パフォーマンス測定・プロファイリングの実践手法
  • クラスタリング・スケーリング戦略
  • メモリ最適化の詳細テクニック

これらの内容を網羅的に学びたい方は、拙著「Node.js開発完全ガイド 2026」をご参照ください。20万文字超のボリュームで、実践的なパフォーマンス最適化手法を詳説しています。


関連記事

GitHubで編集を提案

Discussion