Производительность при type-aware правилах

Type-aware правила ESLint существенно изменяют модель выполнения линтинга в JavaScript/TypeScript-проектах, поскольку переходят от анализа синтаксического дерева к использованию полной типовой информации, предоставляемой компилятором TypeScript. Это расширяет возможности проверок, но вводит значительные издержки на этапе подготовки программы и выполнения правил.

При использовании @typescript-eslint/parser с параметром parserOptions.project ESLint перестаёт работать только с AST и подключает TypeScript Program. Этот режим позволяет правилам обращаться к TypeChecker, извлекать реальные типы выражений, анализировать дженерики, перегрузки функций и контекст вывода типов.

parserOptions: {
  project: ['./tsconfig.json']
}

В этот момент создаётся полноценная инфраструктура компилятора TypeScript:

  • загрузка конфигурации tsconfig
  • разрешение всех файлов проекта
  • построение графа зависимостей
  • создание Program и TypeChecker
  • связывание AST узлов с типовой системой

Именно этот этап является основным источником затрат.

Основные источники падения производительности

Инициализация TypeScript Program

Создание Program — операция, линейно зависящая от количества файлов, включённых в tsconfig. В крупных монорепозиториях это может означать тысячи модулей, даже если ESLint анализирует лишь небольшую часть.

Особенность заключается в том, что TypeScript не создаёт “частичный” program по умолчанию — он стремится собрать полное представление о проекте.

Пересечение нескольких tsconfig

При использовании нескольких конфигураций:

  • tsconfig.app.json
  • tsconfig.test.json
  • tsconfig.node.json

ESLint вынужден создавать отдельный Program для каждого контекста файлов. Это приводит к повторной загрузке одних и тех же исходников и повторному построению графа зависимостей.

Зависимость правил от TypeChecker

Type-aware правила, такие как:

  • @typescript-eslint/no-floating-promises
  • @typescript-eslint/no-misused-promises
  • @typescript-eslint/strict-boolean-expressions

используют checker.getTypeAtLocation() или аналоги. Это делает каждое обращение к типу потенциально дорогим, особенно при глубокой цепочке типов или сложных union/intersection конструкциях.

Отличие syntax-only и type-aware режима

Синтаксический режим ESLint ограничивается структурой AST:

  • проверка токенов
  • анализ структуры кода
  • простые эвристики

Type-aware режим добавляет:

  • разрешение типов
  • вычисление generics
  • анализ контекста вызова
  • проверку совместимости типов

Разница в стоимости выполнения может быть кратной, особенно при включении строгих правил TypeScript ESLint.

Поведение при больших проектах

В монорепозиториях с пакетной структурой основная проблема возникает из-за пересечения:

  • общих node_modules
  • shared типов
  • barrel-файлов (index.ts)
  • re-export цепочек

Каждая такая конструкция увеличивает нагрузку на TypeChecker, так как расширяет область вывода типов.

Дополнительно влияет количество файлов, попадающих в include tsconfig. Даже если ESLint запускается только на src, TypeScript может подтянуть вспомогательные файлы через зависимости.

Оптимизация области анализа

Снижение стоимости type-aware проверки достигается уменьшением зоны действия TypeScript Program.

Ключевым фактором является строгое разделение конфигураций:

  • отдельный tsconfig только для lint
  • исключение тестовых файлов
  • явное ограничение include
{
  "include": ["src/**/*.ts"],
  "exclude": ["**/*.test.ts", "dist", "node_modules"]
}

Чем меньше файлов входит в Program, тем быстрее происходит инициализация и последующие запросы к TypeChecker.

Кэширование ESLint

ESLint поддерживает файловый кэш:

eslint . --cache

Кэширование уменьшает количество повторных вычислений AST и повторного запуска правил. Однако в type-aware режиме важно учитывать, что:

  • изменения tsconfig инвалидируют кэш
  • изменения зависимых файлов могут пересобирать Program
  • кэш не уменьшает стоимость первой инициализации TypeScript

Таким образом, кэш эффективен преимущественно в инкрементальных сценариях CI и локальной разработки.

Разделение правил по окружениям

Практика разделения конфигураций ESLint позволяет минимизировать нагрузку:

  • базовый набор правил без TypeScript Program для редактора
  • расширенный набор type-aware правил для CI

В редакторе часто достаточно синтаксического анализа, поскольку скорость отклика важнее полноты проверки.

В CI, наоборот, допустим полный анализ с type-aware правилами, так как стоимость компенсируется единичным запуском.

Ограничение применения type-aware правил

Не все файлы требуют глубокого анализа типов. Наиболее дорогими являются:

  • тесты с моками сложных типов
  • генераторы схем
  • файлы с heavy generic utilities
  • инфраструктурные слои

Использование overrides позволяет точечно включать типовые правила:

overrides: [
  {
    files: ['src/**/*.ts'],
    parserOptions: {
      project: ['./tsconfig.json']
    }
  }
]

Исключение вспомогательных областей снижает количество обращений к TypeChecker.

Влияние project references

TypeScript Project References изменяет структуру Program, разбивая его на несколько связанных проектов. Это снижает нагрузку за счёт:

  • изоляции зависимостей
  • инкрементальной компиляции
  • переиспользования .tsbuildinfo

Однако ESLint не всегда полностью использует преимущества инкрементальной сборки, поскольку создаёт собственную модель Program. Тем не менее разделение проектов уменьшает размер каждого отдельного графа зависимостей.

Сложность типов и стоимость вычислений

Некоторые конструкции TypeScript оказывают непропорциональное влияние на производительность:

  • глубоко вложенные conditional types
  • рекурсивные generics
  • mapped types с большими union
  • пересечение крупных union-типов

При анализе таких конструкций TypeChecker может выполнять многократные вычисления, особенно при повторных вызовах правил.

Это напрямую влияет на правила ESLint, которые запрашивают тип несколько раз для одного узла AST.

Повторное использование Program

Одной из ключевых оптимизаций является повторное использование TypeScript Program между запусками ESLint. Современные версии typescript-eslint стремятся к этому через internal caching, однако:

  • разные рабочие директории создают новые Program
  • разные tsconfig приводят к пересборке
  • параллельные процессы не разделяют кэш

Поэтому стабильная структура проекта важнее микрооптимизаций.

Ограничение количества type-aware правил

Совокупная стоимость type-aware анализа растёт нелинейно при увеличении количества правил. Причина заключается в том, что:

  • каждое правило может обращаться к TypeChecker
  • одни и те же типы вычисляются многократно
  • отсутствует глобальная дедупликация запросов между правилами

Поэтому набор правил влияет на производительность сильнее, чем кажется при линейной оценке.

Баланс между точностью и скоростью

Type-aware анализ обеспечивает более строгую проверку корректности кода, но требует контроля над инфраструктурой проекта. Основные факторы, влияющие на баланс:

  • размер tsconfig graph
  • глубина типовой системы
  • количество включённых правил
  • структура монорепозитория
  • стратегия кэширования и разделения конфигураций

Поведение ESLint в таком режиме становится ближе к поведению компилятора TypeScript, чем к классическому линтеру, и производительность начинает определяться характеристиками типовой системы проекта, а не только алгоритмами анализа AST.