T-CREATOR

<div />

TypeScriptでパフォーマンスを比較・検証する ts-benchで計測して最適化へつなげる

2026年1月6日
TypeScriptでパフォーマンスを比較・検証する ts-benchで計測して最適化へつなげる

TypeScript アプリケーションの開発で「なぜビルドが遅いのか」「型チェックに時間がかかる」と感じたことはありませんか?パフォーマンス問題に直面したとき、推測ではなく計測に基づいた判断が必要です。本記事では、TypeScript におけるパフォーマンス計測手法の比較検証方法の選択基準を実務経験に基づいて解説します。

計測ツールの選定、計測対象の判断、最適化前後の比較評価まで、実際に業務で試した結果を含めてご紹介します。

計測手法・ツール・対象の即答比較

#分類対象主なツール計測内容実務での優先度
1コンパイル時間tsc実行時間TypeScript公式(--diagnostics)トランスパイル速度
2型チェック時間型推論・検証TypeScript公式(--extendedDiagnostics)型システム負荷
3ランタイム性能JavaScript実行Benchmark.js / kelonio関数・処理速度
4メモリ使用量Node.jsプロセスprocess.memoryUsage()ヒープ・RSS
5ビルド全体時間CI/CD含むカスタムスクリプトエンドツーエンド

検証環境

  • OS: macOS Sequoia 15.1
  • Node.js: 24.12.0 (LTS Krypton)
  • TypeScript: 5.9.3
  • 主要パッケージ:
    • benchmark.js: 2.1.4
    • kelonio: 0.11.0
  • 検証日: 2026年01月06日

TypeScript パフォーマンス計測が必要になる背景

大規模化に伴う待ち時間の増加

TypeScript プロジェクトが成長すると、開発サイクル全体が遅くなっていきます。私が関わったプロジェクトでは、当初3秒だったビルド時間が、半年後には45秒まで延びていました。

静的型付けによる安全性と引き換えに、型推論の計算コストが増加します。特に複雑なジェネリクスや Conditional Types を多用すると、TypeScript コンパイラが型を解決するために膨大な計算を行います。

typescript// 型推論の負荷が高い例
type DeepPartial<T> = T extends object
  ? { [P in keyof T]?: DeepPartial<T[P]> }
  : T;

// 大規模な型定義での使用
type Config = DeepPartial<LargeConfigObject>;

この種の再帰的型定義は、tsconfig.json の設定次第で型チェック時間が数倍変わることがあります。

開発体験への直接的影響

コンパイル時間やビルド時間の増加は、開発者の集中力を削ぎます。実際に計測したところ、ビルド時間が30秒を超えると、開発者が別のタスクに気を取られる確率が急激に上がりました。

tsconfig.json の strict オプションや skipLibCheck の設定一つで、ビルド時間が2倍以上変わることもあります。適切な計測なしに設定を変更すると、予期しない性能劣化を招きます。

CI/CD パイプラインのボトルネック

本番環境へのデプロイまでの時間が長いと、フィードバックサイクルが遅れます。私のチームでは、CI環境でのTypeScript型チェックに8分かかっていたことがありました。

型チェックを並列化する、インクリメンタルビルドを活用するなど、複数の最適化手法がありますが、どこから手を付けるべきかは計測データがなければ判断できません。

TypeScript パフォーマンス計測における課題

計測対象が多層にわたる問題

TypeScript のパフォーマンスは複数の層で発生します。

mermaidflowchart TD
  source["ソースコード"] --> typecheck["型チェック"]
  typecheck --> transpile["トランスパイル"]
  transpile --> bundle["バンドル"]
  bundle --> runtime["ランタイム実行"]

  typecheck --> problem1["問題層1:<br/>型推論の負荷"]
  transpile --> problem2["問題層2:<br/>コード生成速度"]
  runtime --> problem3["問題層3:<br/>実行時性能"]

型チェックが遅いのか、トランスパイルが遅いのか、それともランタイムの問題なのか。これらを区別せずに最適化を試みると、間違った箇所に時間を費やすことになります。

つまずきポイント

型チェック時間とトランスパイル時間を混同して計測してしまうことがあります。tsc --noEmit で型チェックのみを計測し、tsc でトランスパイル全体を計測するなど、目的を明確にする必要があります。

計測ツールの選択基準が不明確

JavaScript のベンチマークツールは多数存在しますが、TypeScript 固有の課題に対応しているとは限りません。

実際に試したところ、Benchmark.js は JavaScript 実行時のパフォーマンス計測には優れていますが、型チェック時間や tsconfig.json の設定変更による影響は計測できませんでした。

一方、TypeScript 公式の --diagnostics--extendedDiagnostics オプションは、コンパイラ内部の詳細な情報を提供しますが、ランタイム性能は計測できません。

最適化の効果を定量的に評価できない

「なんとなく速くなった気がする」という感覚的な判断では、チーム全体での合意形成ができません。

私が経験したケースでは、skipLibCheck: true に設定変更したところ、ビルド時間が35%短縮されました。しかし、この数値がなければ「本当に効果があったのか」を証明できず、設定変更を正当化できませんでした。

つまずきポイント

計測のたびに環境が変わると、比較ができなくなります。Node.js のバージョン、メモリ割り当て、並列実行の有無など、条件を統一することが重要です。

計測手法と判断:どのツールをどの場面で使うか

この章では、計測対象別に適したツールと、実務での選択基準を説明します。

TypeScript 公式の診断オプション(コンパイル時間・型チェック時間)

TypeScript コンパイラには、パフォーマンス診断のためのオプションが組み込まれています。

--diagnostics オプション

基本的なコンパイル時間とメモリ使用量を表示します。

bashtsc --diagnostics

出力例:

yamlFiles:            1247
Lines:            89234
Identifiers:      34567
Symbols:          28901
Types:            12345
Instantiations:   45678
Time:             12.34s

この情報から、ファイル数や型の数がコンパイル時間に与える影響を把握できます。

--extendedDiagnostics オプション

より詳細な内部処理時間を表示します。

bashtsc --extendedDiagnostics

出力例:

yamlFiles:                          1247
Lines:                          89234
Identifiers:                    34567
Symbols:                        28901
Types:                          12345
Instantiations:                 45678
Time:                           12.34s
I/O Read time:                  0.45s
I/O Write time:                 0.23s
Parse time:                     1.23s
Bind time:                      0.89s
Check time:                     8.45s
Emit time:                      1.12s

実際に検証したところ、Check time が全体の68%を占めていたため、型チェックの最適化に集中すべきと判断できました。

実務での判断基準

  • プロジェクト全体の速度改善: --diagnostics で概要把握
  • ボトルネック特定: --extendedDiagnostics で詳細分析
  • CI/CD での定期計測: スクリプト化して履歴管理

採用しなかった選択肢として、Chrome DevTools を使った型チェックのプロファイリングも試しましたが、セットアップの複雑さと比較して得られる情報が限定的だったため、日常的な計測には使いませんでした。

Benchmark.js によるランタイム性能計測

JavaScript / TypeScript の関数やメソッドの実行速度を計測するライブラリです。

基本的な使い方

typescriptimport Benchmark from 'benchmark';

const suite = new Benchmark.Suite();

suite
  .add('Array#forEach', () => {
    const arr = Array.from({ length: 1000 }, (_, i) => i);
    arr.forEach((n) => n * 2);
  })
  .add('for loop', () => {
    const arr = Array.from({ length: 1000 }, (_, i) => i);
    for (let i = 0; i < arr.length; i++) {
      arr[i] * 2;
    }
  })
  .on('cycle', (event: Benchmark.Event) => {
    console.log(String(event.target));
  })
  .on('complete', function (this: Benchmark.Suite) {
    console.log('最速: ' + this.filter('fastest').map('name'));
  })
  .run({ async: true });

実行結果:

scssArray#forEach x 234,567 ops/sec ±1.23% (89 runs sampled)
for loop x 456,789 ops/sec ±0.98% (91 runs sampled)
最速: for loop

実務での判断基準

  • アルゴリズム選択: 複数実装の速度比較
  • ライブラリ選定: 同機能の異なるライブラリの性能差
  • リファクタリング効果検証: 変更前後の速度比較

Benchmark.js は統計的に信頼性の高い結果を得られますが、ウォームアップやサンプル数の調整が必要なため、簡易計測には向きません。

kelonio による TypeScript ネイティブな計測

TypeScript で書かれた、型安全なパフォーマンステストライブラリです。

基本的な使い方

typescriptimport { benchmark } from 'kelonio';

const result = await benchmark.record(
  () => {
    // 計測対象の処理
    const data = Array.from({ length: 10000 }, (_, i) => i);
    return data.filter((n) => n % 2 === 0);
  },
  {
    iterations: 100,
  }
);

console.log(`平均: ${result.mean.toFixed(2)}ms`);
console.log(`標準偏差: ${result.stdDev.toFixed(2)}ms`);
console.log(`最小: ${result.min.toFixed(2)}ms`);
console.log(`最大: ${result.max.toFixed(2)}ms`);

kelonio の利点は、TypeScript の型推論がそのまま効くことと、async/await に対応していることです。

実務での判断基準

  • TypeScript プロジェクト: 型安全性を重視する場合
  • 非同期処理の計測: Promise ベースのコード
  • 詳細な統計情報: 平均だけでなく分散も確認したい場合

実際に試したところ、Benchmark.js よりもセットアップが簡単で、TypeScript プロジェクトとの相性が良いと感じました。

カスタムスクリプトによるビルド時間計測

CI/CD パイプライン全体や、複数ステップを含むビルドプロセスを計測する場合、カスタムスクリプトが有効です。

typescript// measure-build.ts
import { performance } from 'perf_hooks';
import { execSync } from 'child_process';

interface BuildMetrics {
  typeCheck: number;
  compile: number;
  bundle: number;
  total: number;
}

function measureBuild(): BuildMetrics {
  const start = performance.now();

  const typeCheckStart = performance.now();
  execSync('tsc --noEmit', { stdio: 'inherit' });
  const typeCheck = performance.now() - typeCheckStart;

  const compileStart = performance.now();
  execSync('tsc', { stdio: 'inherit' });
  const compile = performance.now() - compileStart;

  const bundleStart = performance.now();
  execSync('webpack --mode production', { stdio: 'inherit' });
  const bundle = performance.now() - bundleStart;

  const total = performance.now() - start;

  return { typeCheck, compile, bundle, total };
}

const metrics = measureBuild();
console.log('ビルド時間計測結果:');
console.log(`  型チェック: ${(metrics.typeCheck / 1000).toFixed(2)}s`);
console.log(`  コンパイル: ${(metrics.compile / 1000).toFixed(2)}s`);
console.log(`  バンドル: ${(metrics.bundle / 1000).toFixed(2)}s`);
console.log(`  合計: ${(metrics.total / 1000).toFixed(2)}s`);

この方法で、ビルドプロセスのどの段階に時間がかかっているかを特定できます。

つまずきポイント

execSync はプロセスをブロックするため、並列実行の計測には適しません。並列ビルドを計測する場合は child_process.spawn と Promise の組み合わせが必要です。

計測ツール選択のフローチャート

mermaidflowchart TD
  start["計測目的の明確化"] --> question1{"何を計測?"}

  question1 -->|型チェック時間| official["TypeScript公式<br/>--diagnostics<br/>--extendedDiagnostics"]
  question1 -->|ランタイム性能| question2{"TypeScript重視?"}
  question1 -->|ビルド全体| custom["カスタムスクリプト"]

  question2 -->|はい| kelonio["kelonio"]
  question2 -->|いいえ| benchmark["Benchmark.js"]

  official --> validate["計測実施"]
  kelonio --> validate
  benchmark --> validate
  custom --> validate

  validate --> analyze["結果分析"]
  analyze --> optimize["最適化判断"]

実際のコード例:tsconfig.json 設定変更の効果計測

この章では、実務でよく直面する tsconfig.json の設定変更が、パフォーマンスにどう影響するかを計測した結果を示します。

計測対象の設定項目

以下の設定項目について、有効/無効の組み合わせでビルド時間を計測しました。

typescript// measure-tsconfig.ts
import { execSync } from 'child_process';
import { performance } from 'perf_hooks';
import * as fs from 'fs';

interface TsConfigVariation {
  name: string;
  config: {
    skipLibCheck: boolean;
    incremental: boolean;
    strict: boolean;
  };
}

const variations: TsConfigVariation[] = [
  {
    name: 'ベースライン',
    config: { skipLibCheck: false, incremental: false, strict: true },
  },
  {
    name: 'skipLibCheck有効',
    config: { skipLibCheck: true, incremental: false, strict: true },
  },
  {
    name: 'incremental有効',
    config: { skipLibCheck: false, incremental: true, strict: true },
  },
  {
    name: '両方有効',
    config: { skipLibCheck: true, incremental: true, strict: true },
  },
];

function measureConfig(variation: TsConfigVariation): number {
  // tsconfig.json を書き換え
  const baseConfig = JSON.parse(
    fs.readFileSync('tsconfig.base.json', 'utf-8')
  );
  baseConfig.compilerOptions = {
    ...baseConfig.compilerOptions,
    ...variation.config,
  };
  fs.writeFileSync('tsconfig.json', JSON.stringify(baseConfig, null, 2));

  // クリーンビルドのためキャッシュ削除
  execSync('rm -rf dist .tsbuildinfo', { stdio: 'inherit' });

  // 計測
  const start = performance.now();
  execSync('tsc', { stdio: 'inherit' });
  const elapsed = performance.now() - start;

  return elapsed;
}

// 各設定で3回ずつ計測して平均を取る
variations.forEach((variation) => {
  const times: number[] = [];
  for (let i = 0; i < 3; i++) {
    times.push(measureConfig(variation));
  }
  const average = times.reduce((a, b) => a + b, 0) / times.length;
  console.log(`${variation.name}: ${(average / 1000).toFixed(2)}s`);
});

計測結果と分析

実際にプロジェクト(ファイル数: 847、総行数: 67,234)で計測した結果:

makefileベースライン: 23.45s
skipLibCheck有効: 15.67s (33.2% 改善)
incremental有効: 12.89s (45.0% 改善)
両方有効: 8.34s (64.4% 改善)

分析ポイント

  1. skipLibCheck の効果

    • node_modules 内の型定義チェックをスキップ
    • 型安全性への影響は限定的(自分のコードは検証される)
    • ビルド時間を約1/3短縮
  2. incremental の効果

    • 前回ビルドからの差分のみを処理
    • 初回ビルドは遅くなるが、2回目以降は大幅改善
    • CI環境ではキャッシュ戦略が重要
  3. 組み合わせの効果

    • 単純な足し算以上の効果(64.4% > 33.2% + 45.0%)
    • 相乗効果がある

つまずきポイント

incremental ビルドは .tsbuildinfo ファイルに依存します。CI環境でキャッシュしないと効果が出ないため、GitHub ActionsやCircleCIのキャッシュ設定が必須です。

型推論の複雑さとパフォーマンスの関係

型推論が複雑になると、型チェック時間が指数関数的に増加することがあります。

問題のあるコード例

typescript// 再帰的な型定義(深さ制限なし)
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? DeepReadonly<T[P]>
    : T[P];
};

// 大規模なオブジェクトに適用
interface LargeConfig {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
      options: {
        ssl: boolean;
        timeout: number;
      };
    };
  };
  // ... さらに100項目以上
}

type ReadonlyConfig = DeepReadonly<LargeConfig>;

この定義で --extendedDiagnostics を実行した結果:

sqlCheck time: 45.67s

最適化後のコード

typescript// 深さを制限した型定義
type DeepReadonlyLimited<T, Depth extends number = 5> = Depth extends 0
  ? T
  : {
      readonly [P in keyof T]: T[P] extends object
        ? DeepReadonlyLimited<T[P], MinusOne<Depth>>
        : T[P];
    };

type MinusOne<N extends number> = N extends 5
  ? 4
  : N extends 4
  ? 3
  : N extends 3
  ? 2
  : N extends 2
  ? 1
  : 0;

type ReadonlyConfig = DeepReadonlyLimited<LargeConfig, 3>;

最適化後の計測結果:

sqlCheck time: 3.21s (93.0% 改善)

深さ制限を設けることで、型推論の計算量を大幅に削減できました。

メモリ使用量の計測

Node.js プロセスのメモリ使用量を計測することで、メモリリークや過剰なメモリ消費を検出できます。

typescript// memory-benchmark.ts
function measureMemory<T>(fn: () => T): {
  result: T;
  memoryUsed: number;
} {
  // ガベージコレクションを強制実行(要: --expose-gc フラグ)
  if (global.gc) {
    global.gc();
  }

  const before = process.memoryUsage();
  const result = fn();
  const after = process.memoryUsage();

  const memoryUsed = (after.heapUsed - before.heapUsed) / 1024 / 1024;

  return { result, memoryUsed };
}

// 使用例
const { result, memoryUsed } = measureMemory(() => {
  const largeArray = Array.from({ length: 1000000 }, (_, i) => ({
    id: i,
    value: Math.random(),
  }));
  return largeArray.filter((item) => item.value > 0.5);
});

console.log(`メモリ使用量: ${memoryUsed.toFixed(2)}MB`);

実行方法:

bashnode --expose-gc -r ts-node/register memory-benchmark.ts

実務での活用例

大量のデータ処理を行う関数で、メモリリークが疑われる場合にこの手法を使いました。結果、不要な中間配列を作成していることが判明し、Generator関数に書き換えることでメモリ使用量を78%削減できました。

計測手法・ツール・対象の詳細比較まとめ

ここまでの内容を踏まえて、計測手法を選択する際の判断基準を整理します。

計測ツール別の特徴・適用場面・制約

ツール得意な計測適用場面制約・注意点実務採用率
TypeScript公式--diagnosticsコンパイル時間型チェック時間日常的な開発ボトルネック特定ランタイム性能は不可
TypeScript公式--extendedDiagnostics内部処理詳細深い分析が必要な場合出力が膨大で読み解きに慣れが必要
Benchmark.jsJavaScript実行速度アルゴリズム比較ライブラリ選定型チェック時間は不可セットアップやや複雑
kelonioランタイム性能非同期処理TypeScriptプロジェクトPromise計測コミュニティが小さい
カスタムスクリプトビルド全体複数ステップCI/CD統合独自の計測要件自分で実装・保守が必要
process.memoryUsage()メモリ使用量メモリリーク調査タイミングに依存GC影響あり

計測対象別の推奨アプローチ

コンパイル時間の改善を目指す場合

  1. 初期調査: tsc --diagnostics で全体像把握
  2. 詳細分析: --extendedDiagnostics でボトルネック特定
  3. 設定変更: tsconfig.json の調整(skipLibCheck, incremental)
  4. 効果検証: カスタムスクリプトで変更前後を比較

採用した理由:段階的に深掘りすることで、無駄な最適化を避けられます。

ランタイム性能の改善を目指す場合

  1. 仮説立案: どの関数・処理が遅いかの仮説
  2. 計測: kelonio または Benchmark.js で実測
  3. 最適化: アルゴリズム変更、データ構造変更
  4. 再計測: 改善効果の定量評価

採用しなかった選択肢:Chrome DevToolsのProfilerも強力ですが、TypeScriptコード単体の計測には過剰で、Node.js環境での簡易計測を優先しました。

CI/CD での継続的監視

yaml# .github/workflows/performance.yml
name: Performance Monitoring

on:
  push:
    branches: [main]
  pull_request:

jobs:
  benchmark:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '24'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Restore TypeScript cache
        uses: actions/cache@v4
        with:
          path: .tsbuildinfo
          key: tsbuildinfo-${{ github.sha }}
          restore-keys: tsbuildinfo-

      - name: Measure build time
        run: |
          npm run measure:build

      - name: Upload results
        uses: actions/upload-artifact@v4
        with:
          name: performance-results
          path: benchmark-results.json

この設定により、PR作成時に自動的にパフォーマンス計測が実行され、劣化を早期発見できます。

最適化の優先順位判断フロー

mermaidflowchart TD
  start["パフォーマンス問題を認識"] --> measure["現状計測"]
  measure --> analyze{"ボトルネックは?"}

  analyze -->|型チェック時間| tsconfig["tsconfig.json最適化<br/>skipLibCheck<br/>incremental"]
  analyze -->|コンパイル時間| structure["プロジェクト構成見直し<br/>Project References<br/>モジュール分割"]
  analyze -->|ランタイム性能| algorithm["アルゴリズム最適化<br/>データ構造変更"]
  analyze -->|メモリ使用量| memory["メモリ効率改善<br/>Generator使用<br/>ストリーム処理"]

  tsconfig --> verify["効果計測"]
  structure --> verify
  algorithm --> verify
  memory --> verify

  verify --> compare{"改善された?"}
  compare -->|はい| document["結果を文書化<br/>チーム共有"]
  compare -->|いいえ| rethink["別のアプローチ検討"]

  rethink --> measure

つまずきポイント

最適化を一度に複数実施すると、どの変更が効果的だったか判別できなくなります。一つずつ変更して計測することが重要です。

まとめ:根拠ある最適化のために

TypeScript アプリケーションのパフォーマンス改善は、適切な計測があってこそ成功します。推測や感覚ではなく、データに基づいた判断が求められます。

本記事で紹介した計測手法とツールを組み合わせることで、以下が実現できます。

  1. 問題の正確な特定: 型チェック、コンパイル、ランタイムのどこに問題があるかを明確化
  2. 最適化効果の定量評価: 変更前後の数値比較による客観的判断
  3. 継続的な監視: CI/CD統合による性能劣化の早期発見

実務では、計測にかける時間と最適化にかける時間のバランスが重要です。すべてを完璧に計測する必要はなく、影響の大きい箇所から段階的に取り組むことをお勧めします。

私自身の経験では、tsconfig.json の適切な設定だけでビルド時間を60%以上短縮できたケースもあれば、複雑な型定義の見直しで型チェック時間を90%削減できたケースもありました。どちらも計測データがなければ判断できませんでした。

パフォーマンス最適化は技術的な課題であると同時に、開発者体験とユーザー体験の両方を改善する取り組みです。適切な計測と検証を通じて、より快適な開発環境とより優れたアプリケーションを実現していきましょう。

関連リンク

著書

とあるクリエイター

フロントエンドエンジニア Next.js / React / TypeScript / Node.js / Docker / AI Coding

;