В энтерпрайз-монорепозиториях с сотнями тысяч строк кода процесс проверки типов (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.
Выполнить tsc --noEmit --extendedDiagnostics и зафиксировать фазы Check time и Parse time.
Запустить tsc --noEmit --generateTrace ./trace_dir для сбора файлов trace.json и types.json.
Проанализировать директорию через @typescript/analyze-trace для получения конкретных номеров строк и ID типов.
Открыть chrome://tracing или ui.perfetto.dev, загрузить trace.json и изучить спаны checkExpression длительностью > 200 мс.
Переписать проблемный тип на интерфейс или ограничить union; повторить замер Check time.
Если в отчете tsc --extendedDiagnostics фаза Check time занимает более 80% времени сборки, а Types count превышает 1.5 миллиона, что это означает?
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), если рекурсивный вызов расположен строго в хвостовой позиции:
// ❌ Неоптимизированный тип: хвостовая рекурсия не работает,
// так как результат рекурсивного вызова оборачивается в кортеж [T[0], ...Reverse<...>]
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 для нахождения проблемного файла и строки.
Замена пересечений типов (&) на интерфейсы, ограничение 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);
}Почему замена анонимных пересечений (type A = B & C) на расширение интерфейсов (interface A extends B, C) ускоряет проверку типов компилятором?
Создание типов путей или селекторов через множественную интерполяцию нескольких крупных 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 в IDE.
Решение: всегда добавлять счетчик глубины рекурсии (Depth extends [unknown, ...infer Rest]) и прерывать вычисление при достижении глубины 5-8 уровней.
Подключение библиотек валидации схем (например, устаревших версий парсеров) может увеличить время сборки проекта втрое из-за неэффективно написанных внутренних маппингов типов.
Решение: запускать tsc --extendedDiagnostics до и после подключения любой новой зависимости.