В ентерпрайз-монорепозиторіях із сотнями тисяч рядків коду процес перевірки типів (tsc --noEmit) нерідко перетворюється на вузьке місце CI/CD пайплайнів, розтягуючись на десятки хвилин і споживаючи гігабайти оперативної пам'яті.
Причина падіння продуктивності компілятора TypeScript криється в математичній природі його системи типів. TypeScript використовує структурну типізацію (Duck Typing) із підтримкою узагальнених типів вищого порядку, рекурсивних умовних типів та розподілених об'єднань (Distributive Conditional Types). Система перевірки типів є Тюрінг-повною. Необачно спроєктований тип здатен викликати комбінаторний вибух типів (Combinatorial Explosion), перетворюючи перевірку одного виразу на мільйони внутрішніх зіставлень.
Для систематичного пошуку та ліквідації вузьких місць компілятор TypeScript оснащений вбудованим інструментарієм телеметрії:
Життєвий цикл компіляції програми в tsc складається з п'яти послідовних фаз, кожна з яких відображається в діагностичному звіті:
+-------------------------------------------------------------------------+
| ФАЗИ КОМПІЛЯЦІЇ TYPESCRIPT (TSC PIPELINE) |
+-------------------------------------------------------------------------+
| 1. I/O & PARSE: Читання файлів із диска та побудова AST |
| Files: 2,450 | Parse time: 1.2s |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| 2. BIND: Зв'язування вузлів AST із таблицею символів (Symbol Table) |
| Symbols: 480,210 | Bind time: 0.4s |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| 3. CHECK (TYPE-CHECKER): Перевірка типів та структурної сумісності |
| Types: 1,890,000 | Check time: 18.6s <-- ОСНОВНЕ ВУЗЬКЕ МІСЦЕ |
| (Тут відбуваються ресурсомісткі обчислення union-перетинів) |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| 4. EMIT: Генерація JavaScript та .d.ts файлів |
| Emit time: 0.0s (із прапорцем --noEmit) |
+-------------------------------------------------------------------------+Якщо фаза Check time становить понад 70% від загального часу компіляції, а кількість створених типів (Types count) перевищує мільйон, у кодовій базі присутні аномально важкі типи, що вимагають профілювання через --generateTrace.
asChild, Slot), що блокували автодоповнення в IDE сотень інженерів.Type instantiation is excessively deep).--extendedDiagnostics#Для зняття базового профілю запускається компілятор із прапорцем розширеної діагностики:
pnpm exec tsc --noEmit --extendedDiagnosticsПриклад виводу та контрольні орієнтири:
Files: 2180
Lines of Library: 325010
Lines of Definitions: 89120
Lines of TypeScript: 215430
Nodes: 1840290
Identifiers: 620100
Symbols: 810450
Types: 2410980 <-- КРИТИЧНО: понад 1.5M типів свідчить про вибух складності
Instantiations: 5210340 <-- КРИТИЧНО: інстанціювання узагальнених типів
Memory used: 1420500K <-- Споживання пам'яті (1.4 GB)
I/O read: 0.42s
I/O write: 0.00s
Parse time: 1.85s
Bind time: 0.55s
Check time: 24.10s <-- 85% часу компіляції!
Emit time: 0.00s
Total time: 26.92s--generateTrace#Для детальної локалізації винуватця компілятор запускається із зазначенням директорії трасування:
pnpm exec tsc --noEmit --generateTrace ./ts-traceУ директорії ./ts-trace створюються два файли:
trace.json: перелік подій у форматі Chrome Event Tracing (виклики функцій компілятора: checkSourceFile, checkExpression, structuredTypeRelatedTo).types.json: словник відповідності числових ідентифікаторів типів (id) їхньому текстовому представленню.Для автоматичного аналізу артефактів трасування використовується офіційна утиліта Microsoft @typescript/analyze-trace:
pnpm dlx @typescript/analyze-trace ./ts-traceУтиліта генерує звіт про найбільш ресурсомісткі типи та файли, ранжуючи їх за часом перевірки:
Hot Spots:
Check time: 8,420ms (file: src/shared/api/schema.ts, line: 142)
Type ID: 41209
Type Representation: DeepMerge<UserProfile, TenantSettings & UserPermissions>
Reason: structuredTypeRelatedTo comparison took 8.4s across 42,000 sub-comparisons.Одне з головних джерел уповільнення компілятора — надмірне використання перетинів типів (&) замість спадкування інтерфейсів (interface extends).
Інтерфейси кешують структурні зв'язки за своїм унікальним іменем (Nominal/Identifier cache). При перевірці сумісності двох інтерфейсів TypeScript миттєво перевіряє збіг їхніх імен. Навпаки, перетин анонімних типів (type A = B & C) примушує компілятор щоразу рекурсивно порівнювати кожну властивість:
// ❌ Повільний патерн: перетин типів не кешується
type BaseEntity = { id: string; createdAt: Date; updatedAt: Date };
type AuditInfo = { createdBy: string; version: number };
type Permissions = { canRead: boolean; canWrite: boolean; canDelete: boolean };
// При кожному зверненні компілятор розраховує пласку структуру заново
export type UserAggregate = BaseEntity & AuditInfo & Permissions & {
email: string;
displayName: string;
};
// ✅ Швидкий enterprise-патерн: інтерфейси з кешованим деревом спадкування
interface BaseEntityInterface {
id: string;
createdAt: Date;
updatedAt: Date;
}
interface AuditInfoInterface {
createdBy: string;
version: number;
}
interface PermissionsInterface {
canRead: boolean;
canWrite: boolean;
canDelete: boolean;
}
// Компілятор кешує сигнатуру UserAggregateInterface в єдиний хеш-ідентифікатор
export interface UserAggregateInterface
extends BaseEntityInterface,
AuditInfoInterface,
PermissionsInterface {
email: string;
displayName: string;
}До версії TypeScript 4.5 рекурсивні умовні типи швидко вичерпували ліміт глибини стека (за замовчуванням 50 кроків). Починаючи з TS 4.5, компілятор підтримує оптимізацію хвостової рекурсії (Tail-Call Elimination), якщо рекурсивний виклик розташований строго у хвостовій позиції:
// ❌ Неоптимізований тип: хвостова рекурсія не працює,
// оскільки результат рекурсивного виклику обертається в кортеж [...ReverseSlow<Tail>, Head]
type ReverseSlow<T extends unknown[]> = T extends [infer Head, ...infer Tail]
? [...ReverseSlow<Tail>, Head]
: [];
// ✅ Оптимізований тип з акумулятором у хвостовій позиції (TCO):
// Компілятор виконує ітеративне розгортання до глибини 1000 елементів без накладних витрат
type ReverseFast<T extends unknown[], Acc extends unknown[] = []> =
T extends [infer Head, ...infer Tail]
? ReverseFast<Tail, [Head, ...Acc]>
: Acc;Запуск tsc --noEmit --extendedDiagnostics зі збереженням Check time та Types count у метадані збірки.
Якщо Check time зріс більш ніж на 25% порівняно з main-гілкою, автоматичний запуск --generateTrace.
Аналіз трасування через @typescript/analyze-trace для знаходження проблемного файлу, рядка та ID типу.
Заміна перетинів типів (&) на інтерфейси, звуження union-множин, застосування TCO до рекурсивних типів.
Нижче наведено production-скрипт для моніторингу часу компіляції в CI/CD із блокуванням PR при перевищенні порогу складності типів:
import { execSync } from 'node:child_process';
const MAX_ALLOWED_CHECK_TIME_SEC = 15.0;
const MAX_ALLOWED_TYPES_COUNT = 1_000_000;
console.log('🚀 Запуск розширеної діагностики TypeScript компілятора...');
try {
const output = execSync('pnpm exec tsc --noEmit --extendedDiagnostics', {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
const getMetric = (pattern) => {
const match = output.match(pattern);
return match ? match[1].trim() : null;
};
const checkTimeStr = getMetric(/Check time:\s+([\d.]+)s/);
const typesCountStr = getMetric(/Types:\s+(\d+)/);
const memoryUsedStr = getMetric(/Memory used:\s+(\d+)K/);
const checkTime = parseFloat(checkTimeStr || '0');
const typesCount = parseInt(typesCountStr || '0', 10);
const memoryMb = Math.round(parseInt(memoryUsedStr || '0', 10) / 1024);
console.log(`⏱️ Час перевірки типів (Check time): ${checkTime}s`);
console.log(`📊 Кількість створених типів: ${typesCount.toLocaleString()}`);
console.log(`💾 Споживання оперативної пам'яті: ${memoryMb} MB`);
let hasRegression = false;
if (checkTime > MAX_ALLOWED_CHECK_TIME_SEC) {
console.error(
`❌ ПОМИЛКА: Check time (${checkTime}s) перевищує допустимий ліміт (${MAX_ALLOWED_CHECK_TIME_SEC}s)!`
);
hasRegression = true;
}
if (typesCount > MAX_ALLOWED_TYPES_COUNT) {
console.error(
`❌ ПОМИЛКА: Кількість типів (${typesCount.toLocaleString()}) перевищує поріг (${MAX_ALLOWED_TYPES_COUNT.toLocaleString()})!`
);
hasRegression = true;
}
if (hasRegression) {
console.log('\n💡 Запустіть локально: pnpm exec tsc --noEmit --generateTrace ./trace-output');
console.log('Потім дослідіть звіт: pnpm dlx @typescript/analyze-trace ./trace-output\n');
process.exit(1);
}
console.log('✅ Перевірка продуктивності типів успішно пройдена.');
} catch (error) {
console.error('Не вдалося виконати компіляцію або перевірку типів:');
if (error.stdout) console.error(error.stdout);
if (error.stderr) console.error(error.stderr);
process.exit(1);
}Створення типів шляхів чи селекторів шляхом множинної інтерполяції кількох широких union-типів призводить до миттєвого переповнення пам'яті компілятора: A * B * C * D комбінацій.
// ❌ Антипатерн: генерація 50 * 50 * 50 = 125 000 варіантів рядкових літералів!
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type Entity = 'users' | 'orders' | 'products' | 'invoices' | 'roles';
type SubRoute = 'list' | 'detail' | 'create' | 'update' | 'delete' | 'export';
// Компілятор намагається створити всі комбінації в єдиному типі:
type APIRoute = `/${Entity}/${SubRoute}`; // Вибухове зростання типів!
// ✅ Правильне рішення: динамічна валідація через узагальнені функції без попередньої генерації мега-union
function buildRoute<E extends Entity, S extends SubRoute>(entity: E, sub: S): `/${E}/${S}` {
return `/${entity}/${sub}`;
}Використання динамічних типів Record<string, unknown> у поєднанні з рекурсивним пошуком ключів об'єктів без обмеження максимальної глибини (Depth Limiting) призводить до зависання Language Server у редакторі коду.
Рішення: завжди додавати лічильник глибини рекурсії (Depth extends [unknown, ...infer Rest]) і зупиняти обчислення при досягненні глибини 5-8 рівнів.
Підключення сторонніх бібліотек валідації схем із неоптимальними рекурсивними типами може потроїти час компіляції проєкту.
Рішення: запускати tsc --extendedDiagnostics до та після додавання будь-якої нової залежності.
Виконати tsc --noEmit --extendedDiagnostics та зафіксувати фази Check time і Parse time.
Запустити tsc --noEmit --generateTrace ./trace_dir для збору файлів trace.json та types.json.
Проаналізувати артефакти через @typescript/analyze-trace для отримання точних номерів рядків та ID типів.
Відкрити ui.perfetto.dev, завантажити trace.json та дослідити спани checkExpression тривалістю понад 200 мс.
Переписати проблемний тип на інтерфейс або обмежити union; повторити замір Check time.