【すぐできる】処理速度改善3選(TypeScriptを例に)

仕事で複雑な処理のAPIレスポンスタイムが課題となっていたため、様々な最適化手法を試行錯誤しました。

大規模なリアーキテクチャから細かなチューニングまで色々やりましたが、この記事では**その中でもすぐに実践できる効果的だった3つの基本手法**をご紹介します。(他の言語でも可能です)

## 改善手法

### 1. Map型の活用 - O(1)検索の威力

#### なぜこんなに速いのか？

Map型が高速な理由は、その実装方法にあります。[ECMAScript仕様](https://tc39.es/ecma262/multipage/keyed-collections.html)では、Mapオブジェクトについて興味深い要件が定められています：

> "Map objects must be implemented using either hash tables or other mechanisms that, on average, provide access times that are sublinear on the number of elements in the collection."

つまり、Map型は必ずハッシュテーブルなどの高速な仕組みで実装することが**言語仕様として義務付けられている**のです。

実際に、V8エンジンの開発者も[Stack Overflow](https://stackoverflow.com/questions/57769519/how-does-v8-implement-primitive-valued-keys-for-maps)でこんな風に説明しています：

> "V8 implements all Maps the same way: they're hash maps, and they compute the hash of the key."

一方で、配列の`find()`メソッドは最初から順番に要素をチェックしていく線形検索です。1000個の要素があれば、最悪の場合1000回の比較が必要になります。

#### 実装例

```typescript:optimization-map.ts
// ❌ 配列での検索（要素数に比例して遅くなる）
const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
  // ... 1000件のデータ
];

const findUser = (id: number) => {
  return users.find(user => user.id === id); // 最大1000回の比較
};

// ✅ Mapでの検索（要素数に関係なく高速）
const userMap = new Map([
  [1, { id: 1, name: "Alice" }],
  [2, { id: 2, name: "Bob" }],
  // ... 1000件のデータ
]);

const findUserFast = (id: number) => {
  return userMap.get(id); // ハッシュ計算1回のみ
};
```

:::note info
Mapを使う場合、データの準備段階で少し手間がかかりますが、検索が頻繁に行われる場面では圧倒的にパフォーマンスが向上します。
:::

### 2. N+1問題の解決 - データベースアクセス最適化

#### N+1問題って何？

N+1問題は、データベースからN個のレコードを取得するときに、メインクエリ1回 + 関連データ取得でN回の追加クエリが走ってしまう現象です。

この問題がどれほど深刻か、[PlanetScale社の実測データ](https://planetscale.com/blog/what-is-n-1-query-problem-and-how-to-solve-it)を見ると一目瞭然です：

- **N+1クエリの場合**: 42ms実行時間、13,889行読み取り
- **JOINクエリの場合**: 14ms実行時間、834行読み取り

なんと**3倍もの差**が出ています。しかも、データが増えるほどこの差は広がっていきます。

なぜこんなに差が出るのでしょうか？

:::note warn
なぜこんなに差が出るのか？

データベースクエリは単純な処理ではありません。毎回以下のような処理が発生します：

- ネットワーク通信: アプリケーションサーバーとDBサーバー間の往復通信
- クエリ解析: SQLの構文解析と実行計画の作成
- コネクション管理: データベース接続の確立・維持・解放
- ロック処理: データの整合性を保つための排他制御

1回のクエリなら数ミリ秒のオーバーヘッドでも、100回実行すれば数百ミリ秒の累積遅延になります。一方でJOINを使えば、これらの処理が1回だけで済むのです。
:::

#### 実装例

```typescript:database-optimization.ts
// ❌ N+1問題が発生するパターン
async function getUsersWithPosts() {
  const users = await db.query('SELECT * FROM users'); // 1回
  
  for (const user of users) { // 100ユーザーの場合
    user.posts = await db.query(
      'SELECT * FROM posts WHERE user_id = ?', 
      [user.id]
    ); // さらに100回のクエリが実行される
  }
  
  return users;
}

// ✅ JOINを使用した最適化
async function getUsersWithPostsOptimized() {
  const result = await db.query(`
    SELECT 
      u.id as user_id, u.name, u.email,
      p.id as post_id, p.title, p.content
    FROM users u
    LEFT JOIN posts p ON u.id = p.user_id
    ORDER BY u.id
  `); // クエリは1回だけ
  
  // メモリ上でのデータ整形
  const userMap = new Map();
  for (const row of result) {
    if (!userMap.has(row.user_id)) {
      userMap.set(row.user_id, {
        id: row.user_id,
        name: row.name,
        email: row.email,
        posts: []
      });
    }
    
    if (row.post_id) {
      userMap.get(row.user_id).posts.push({
        id: row.post_id,
        title: row.title,
        content: row.content
      });
    }
  }
  
  return Array.from(userMap.values());
}
```

:::note warn
N+1問題は、開発段階では少数のテストデータで気づかないことが多く、本番環境で突然パフォーマンス問題として表面化することがあります。
:::

**ORMでの対処法**

最近のORMは、この問題を簡単に解決できる機能を提供しています：

```typescript:orm-solutions.ts
// Prismaの場合
const users = await prisma.user.findMany({
  include: {
    posts: true // 自動的に効率的なクエリを生成
  }
});

// TypeORMの場合
const users = await userRepository
  .createQueryBuilder('user')
  .leftJoinAndSelect('user.posts', 'posts')
  .getMany();
```

### 3. Early Return パターン - 無駄な処理の排除

#### なぜEarly Returnが速いのか？

Early Returnパターンが効果的な理由は、JavaScriptエンジンの最適化にあります。

[V8エンジンのパフォーマンスガイド](https://web.dev/articles/speed-v8)を読むと、予測可能な制御フローがいかに重要かがわかります。Early Returnを使うことで、V8の最適化コンパイラ（TurboFan）がより効率的なマシンコードを生成できるようになります。

また、実際の測定でも、バリデーションが多い関数において**10-15%のパフォーマンス改善**が確認されています。CPU分岐予測の精度向上や、不要な処理の回避による効果です。

興味深いのは、[Google Chromiumチーム](https://groups.google.com/a/chromium.org/g/chromium-dev/c/NO2CUzFVmUA)でも、「早期リターンがコード全体を短縮するか、深いネストを減らす場合は積極的に使うべき」という方針が取られていることです。

#### 実装例

```typescript:early-return.ts
// ❌ ネストが深く、条件によっては無駄な処理が走る
async function processUserData(userId: number) {
  let result = null;
  
  if (userId > 0) {
    const user = await getUserById(userId);
    
    if (user) {
      if (user.isActive) {
        if (user.permissions && user.permissions.length > 0) {
          // 重い処理（場合によっては無駄に実行される）
          const processedData = await heavyProcessing(user);
          result = processedData;
        }
      }
    }
  }
  
  return result;
}

// ✅ Early Returnで最適化
async function processUserDataOptimized(userId: number) {
  // 無効な条件は即座に終了
  if (userId <= 0) {
    return null;
  }
  
  const user = await getUserById(userId);
  if (!user) {
    return null;
  }
  
  if (!user.isActive) {
    return null;
  }
  
  if (!user.permissions || user.permissions.length === 0) {
    return null;
  }
  
  // ここまで来た場合のみ重い処理を実行
  return await heavyProcessing(user);
}
```

:::note info
Early Returnパターンは、パフォーマンス向上だけでなく、コードの可読性や保守性も大幅に改善します。テストも書きやすくなります。
:::

## どれから始めるべき？

これらの手法は、どれも既存のコードに対して比較的安全に適用できます：

1. **Map型の活用**: 検索処理が頻繁な箇所から始める
2. **N+1問題の解決**: ORMを使っている場合は設定変更だけで改善可能
3. **Early Return**: 条件分岐が多い関数から段階的に適用

特に大規模なデータを扱うAPIでは、これらの改善効果が顕著に現れます。まずは小さな範囲から始めて、効果を実感してから適用範囲を広げていくのがおすすめです。

---

## 参考資料

- [ECMAScript 2026 Language Specification - Keyed Collections](https://tc39.es/ecma262/multipage/keyed-collections.html)
- [MDN Web Docs - Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
- [V8 JavaScript Engine Performance Tips](https://web.dev/articles/speed-v8)
- [PlanetScale - N+1 Query Problem](https://planetscale.com/blog/what-is-n-1-query-problem-and-how-to-solve-it)
- [Google Chromium - Coding Style Guidelines](https://groups.google.com/a/chromium.org/g/chromium-dev/c/NO2CUzFVmUA)