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% 改善)
分析ポイント
-
skipLibCheck の効果
- node_modules 内の型定義チェックをスキップ
- 型安全性への影響は限定的(自分のコードは検証される)
- ビルド時間を約1/3短縮
-
incremental の効果
- 前回ビルドからの差分のみを処理
- 初回ビルドは遅くなるが、2回目以降は大幅改善
- CI環境ではキャッシュ戦略が重要
-
組み合わせの効果
- 単純な足し算以上の効果(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.js | JavaScript実行速度 | アルゴリズム比較ライブラリ選定 | 型チェック時間は不可セットアップやや複雑 | 高 |
| kelonio | ランタイム性能非同期処理 | TypeScriptプロジェクトPromise計測 | コミュニティが小さい | 中 |
| カスタムスクリプト | ビルド全体複数ステップ | CI/CD統合独自の計測要件 | 自分で実装・保守が必要 | 高 |
| process.memoryUsage() | メモリ使用量 | メモリリーク調査 | タイミングに依存GC影響あり | 中 |
計測対象別の推奨アプローチ
コンパイル時間の改善を目指す場合
- 初期調査:
tsc --diagnosticsで全体像把握 - 詳細分析:
--extendedDiagnosticsでボトルネック特定 - 設定変更: tsconfig.json の調整(skipLibCheck, incremental)
- 効果検証: カスタムスクリプトで変更前後を比較
採用した理由:段階的に深掘りすることで、無駄な最適化を避けられます。
ランタイム性能の改善を目指す場合
- 仮説立案: どの関数・処理が遅いかの仮説
- 計測: kelonio または Benchmark.js で実測
- 最適化: アルゴリズム変更、データ構造変更
- 再計測: 改善効果の定量評価
採用しなかった選択肢: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 アプリケーションのパフォーマンス改善は、適切な計測があってこそ成功します。推測や感覚ではなく、データに基づいた判断が求められます。
本記事で紹介した計測手法とツールを組み合わせることで、以下が実現できます。
- 問題の正確な特定: 型チェック、コンパイル、ランタイムのどこに問題があるかを明確化
- 最適化効果の定量評価: 変更前後の数値比較による客観的判断
- 継続的な監視: CI/CD統合による性能劣化の早期発見
実務では、計測にかける時間と最適化にかける時間のバランスが重要です。すべてを完璧に計測する必要はなく、影響の大きい箇所から段階的に取り組むことをお勧めします。
私自身の経験では、tsconfig.json の適切な設定だけでビルド時間を60%以上短縮できたケースもあれば、複雑な型定義の見直しで型チェック時間を90%削減できたケースもありました。どちらも計測データがなければ判断できませんでした。
パフォーマンス最適化は技術的な課題であると同時に、開発者体験とユーザー体験の両方を改善する取り組みです。適切な計測と検証を通じて、より快適な開発環境とより優れたアプリケーションを実現していきましょう。
関連リンク
著書
article2026年1月23日TypeScriptのtypeとinterfaceを比較・検証する 違いと使い分けの判断基準を整理
article2026年1月23日TypeScript 5.8の型推論を比較・検証する 強化点と落とし穴の回避策
article2026年1月23日TypeScript Genericsの使用例を早見表でまとめる 記法と頻出パターンを整理
article2026年1月22日TypeScriptの型システムを概要で理解する 基礎から全体像まで完全解説
article2026年1月22日ZustandとTypeScriptのユースケース ストアを型安全に設計して運用する実践
article2026年1月22日TypeScriptでよく出るエラーをトラブルシュートでまとめる 原因と解決法30選
articleshadcn/ui × TanStack Table 設計術:仮想化・列リサイズ・アクセシブルなグリッド
articleRemix のデータ境界設計:Loader・Action とクライアントコードの責務分離
articlePreact コンポーネント設計 7 原則:再レンダリング最小化の分割と型付け
articlePHP 8.3 の新機能まとめ:readonly クラス・型強化・性能改善を一気に理解
articleNotebookLM に PDF/Google ドキュメント/URL を取り込む手順と最適化
articlePlaywright 並列実行設計:shard/grep/fixtures で高速化するテストスイート設計術
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
