← Курс/Производительность TypeScript компилятора#197 из 257+20 XP

Производительность TypeScript компилятора

Проблемы производительности

TypeScript компилятор может замедляться при:

  • Глубоко вложенных дженериках
  • Рекурсивных условных типах
  • Огромных проектах без кэширования
  • Проверке типов из node_modules
  • skipLibCheck: быстрый выигрыш

    {
      "compilerOptions": {
        "skipLibCheck": true
      }
    }

    Пропускает проверку типов в .d.ts файлах зависимостей. Безопасно в большинстве случаев — при конфликтах версий типов экономит минуты.

    isolatedModules: параллельная компиляция

    {
      "compilerOptions": {
        "isolatedModules": true
      }
    }

    Каждый файл компилируется независимо — это позволяет использовать быстрые транспайлеры (esbuild, swc, Babel):

    // Запрещено с isolatedModules:
    const enum Status { Active, Inactive }  // const enum требует межфайловый анализ
    export { type User }                    // type-only re-export в некоторых случаях
    
    // Разрешено:
    enum Status { Active, Inactive }  // обычный enum OK
    export type { User }              // явный type export OK

    incremental: кэширование сборки

    {
      "compilerOptions": {
        "incremental": true,
        "tsBuildInfoFile": ".tsbuildinfo"
      }
    }

    TypeScript сохраняет информацию о предыдущей сборке и перекомпилирует только изменённые файлы. На больших проектах ускорение в 3-10 раз.

    Избегайте глубоких вложенных дженериков

    // Медленно — глубокая рекурсия типов:
    type DeepReadonly<T> = {
      readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]
    }
    
    // Лучше — ограничить глубину или использовать intersections:
    type Readonly1<T> = { readonly [K in keyof T]: T[K] }
    
    // Проблема: Type instantiation is excessively deep (ошибка TS2589)
    // Решение: добавить extends any[] или базовый случай рекурсии
    type Flatten<T> = T extends Array<infer Item> ? Item : T

    Измерение производительности

    # Профилировщик TypeScript:
    tsc --extendedDiagnostics
    
    # Вывод покажет:
    # Files:              150
    # Lines:            25000
    # Identifiers:      18000
    # Check time:       2.5s   <- основное время
    # Output generation: 0.3s
    
    # Ещё детальнее:
    tsc --generateTrace /tmp/ts-trace
    # Открыть в chrome://tracing

    project references для монорепо

    # Вместо компиляции всего:
    tsc --build  # перекомпилирует только изменённые пакеты

    Советы по оптимизации

    1. Используйте type-only imports:

    import type { User } from './types'  // не попадёт в runtime bundle

    2. Избегайте re-export всего:

    // Медленно (TypeScript должен загрузить весь модуль):
    export * from './bigModule'
    
    // Быстрее — явный re-export нужного:
    export { SpecificThing } from './bigModule'

    3. paths и baseUrl — не злоупотребляйте:

    Большое количество path aliases замедляет разрешение модулей.

    4. Разбейте большие union типы:

    // Медленно: 100+ вариантов в union
    type BigUnion = 'option1' | 'option2' | ... | 'option100'
    
    // Быстрее: string с enum/const object для валидации
    const OPTIONS = { OPTION1: 'option1', OPTION2: 'option2' } as const
    type Option = typeof OPTIONS[keyof typeof OPTIONS]

    Примеры

    Измерение производительности: бенчмарк компиляции, кэширование результатов (как incremental), симуляция isolatedModules

    // Симулируем концепции производительности TypeScript компилятора.
    // Демонстрируем incremental builds, кэширование и замеры времени.
    
    // --- Симуляция incremental компиляции ---
    class IncrementalCompiler {
      constructor() {
        this.cache = new Map()  // path -> { hash, result, time }
        this.totalFiles = 0
        this.cachedFiles = 0
        this.compiledFiles = 0
      }
    
      // Симуляция хэша файла (в реальности — хэш содержимого)
      _hashFile(path, content) {
        let hash = 0
        for (let i = 0; i < content.length; i++) {
          hash = ((hash << 5) - hash) + content.charCodeAt(i)
          hash |= 0
        }
        return hash.toString(36)
      }
    
      // Симуляция компиляции файла (как tsc с типами)
      _compile(path, content) {
        // Имитируем время компиляции пропорционально размеру
        const startTime = performance ? performance.now() : Date.now()
        // Дорогая операция: traverse AST, type-check, emit
        let result = ''
        for (let i = 0; i < content.length; i++) {
          result += content[i]
        }
        const elapsed = (performance ? performance.now() : Date.now()) - startTime
        return { js: result, declarations: result + '.d.ts', elapsed }
      }
    
      compile(files) {
        const results = {}
        this.totalFiles = files.length
    
        for (const { path, content } of files) {
          const hash = this._hashFile(path, content)
          const cached = this.cache.get(path)
    
          if (cached && cached.hash === hash) {
            // Cache hit — используем кэш (как incremental build)
            results[path] = { ...cached.result, fromCache: true }
            this.cachedFiles++
          } else {
            // Cache miss — компилируем
            const result = this._compile(path, content)
            this.cache.set(path, { hash, result })
            results[path] = { ...result, fromCache: false }
            this.compiledFiles++
          }
        }
    
        return results
      }
    
      getStats() {
        return {
          total: this.totalFiles,
          compiled: this.compiledFiles,
          fromCache: this.cachedFiles,
          cacheHitRate: this.totalFiles > 0
            ? Math.round((this.cachedFiles / this.totalFiles) * 100) + '%'
            : '0%'
        }
      }
    
      reset() {
        this.totalFiles = 0
        this.cachedFiles = 0
        this.compiledFiles = 0
      }
    }
    
    // --- Симуляция глубоких дженериков vs простых ---
    function measureTypeComplexity(typeName, recursionDepth) {
      // Симулируем стоимость вычисления типа
      // Глубокая рекурсия = экспоненциальный рост
      const cost = Math.pow(2, recursionDepth) * 0.1  // мс
      return { typeName, recursionDepth, estimatedMs: cost.toFixed(2) }
    }
    
    // --- Симуляция isolatedModules проверки ---
    function checkIsolatedModules(fileContent) {
      const violations = []
    
      // const enum не поддерживается в isolatedModules
      if (/consts+enums+/.test(fileContent)) {
        violations.push('const enum нельзя использовать с isolatedModules')
      }
    
      // namespace с implementation
      if (/namespaces+w+s*{[sS]*?(?:class|function|let|const|var)/.test(fileContent)) {
        violations.push('namespace с implementation несовместим с isolatedModules')
      }
    
      return {
        valid: violations.length === 0,
        violations
      }
    }
    
    // --- Демонстрация ---
    
    const compiler = new IncrementalCompiler()
    
    const projectFiles = [
      { path: 'utils/date.ts', content: 'export function formatDate(d) { return d.toISOString() }' },
      { path: 'utils/math.ts', content: 'export function add(a, b) { return a + b }' },
      { path: 'services/api.ts', content: 'import { formatDate } from "../utils/date"; export const api = {}' },
      { path: 'App.ts', content: 'import { api } from "./services/api"; export default function App() {}' },
    ]
    
    console.log('=== Первая сборка (без кэша) ===')
    compiler.compile(projectFiles)
    let stats = compiler.getStats()
    console.log('Скомпилировано:', stats.compiled, 'файлов')
    console.log('Из кэша:', stats.fromCache, 'файлов')
    console.log('Cache hit rate:', stats.cacheHitRate)
    
    // Вторая сборка — только один файл изменился
    compiler.reset()
    const updatedFiles = [
      ...projectFiles.slice(0, 3),  // первые 3 не изменились
      { path: 'App.ts', content: 'import { api } from "./services/api"; export default function App() { return "updated" }' },
    ]
    
    console.log('\n=== Вторая сборка (один файл изменился) ===')
    compiler.compile(updatedFiles)
    stats = compiler.getStats()
    console.log('Скомпилировано:', stats.compiled, 'файлов (только изменённые)')
    console.log('Из кэша:', stats.fromCache, 'файлов')
    console.log('Cache hit rate:', stats.cacheHitRate)
    
    console.log('\n=== Третья сборка (ничего не изменилось) ===')
    compiler.reset()
    compiler.compile(updatedFiles)
    stats = compiler.getStats()
    console.log('Скомпилировано:', stats.compiled, 'файлов')
    console.log('Из кэша:', stats.fromCache, 'файлов')
    console.log('Cache hit rate:', stats.cacheHitRate)
    
    console.log('\n=== Стоимость дженерик-типов ===')
    const types = [
      { name: 'Readonly<T>', depth: 1 },
      { name: 'DeepReadonly<T>', depth: 5 },
      { name: 'DeepReadonly<T> (глубокий)', depth: 10 },
      { name: 'Рекурсивный тип', depth: 15 },
    ]
    types.forEach(({ name, depth }) => {
      const result = measureTypeComplexity(name, depth)
      console.log(`  ${name}: ~${result.estimatedMs}ms (глубина ${depth})`)
    })
    
    console.log('\n=== Проверка isolatedModules ===')
    const files_to_check = [
      { name: 'valid.ts', content: 'export type { User }; enum Status { Active }' },
      { name: 'invalid.ts', content: 'export const enum Status { Active, Inactive }' },
    ]
    files_to_check.forEach(({ name, content }) => {
      const { valid, violations } = checkIsolatedModules(content)
      console.log(`  ${name}: ${valid ? 'OK' : 'VIOLATION: ' + violations[0]}`)
    })