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

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

Для получения сведений о типах ESLint взаимодействует с компилятором TypeScript через объект Program. Именно эта интеграция позволяет обнаруживать ошибки, которые невозможно выявить на уровне обычного синтаксического анализа.

Примеры type-aware правил из пакета @typescript-eslint/eslint-plugin:

  • no-floating-promises
  • await-thenable
  • no-misused-promises
  • restrict-plus-operands
  • restrict-template-expressions
  • no-unnecessary-type-assertion
  • prefer-nullish-coalescing
  • unbound-method

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


Почему type-aware правила работают медленнее

Обычный ESLint анализирует файл изолированно:

  1. Парсит исходный код.
  2. Строит AST (Abstract Syntax Tree).
  3. Запускает набор правил.
  4. Формирует отчёт.

При использовании type-aware правил процесс усложняется:

  1. Загружается конфигурация TypeScript.
  2. Строится TypeScript Program.
  3. Выполняется разрешение импортов.
  4. Анализируются зависимости между файлами.
  5. Вычисляются типы выражений.
  6. Создаются дополнительные структуры данных для доступа к Type Checker.
  7. Только после этого запускаются правила.

С точки зрения затрат ресурсов происходит переход от анализа отдельного файла к анализу значительной части проекта.


Роль TypeScript Program

Ключевым фактором производительности становится создание объекта Program.

Упрощённо процесс выглядит следующим образом:

ESLint
   ↓
typescript-eslint parser
   ↓
tsconfig.json
   ↓
TypeScript Program
   ↓
Type Checker
   ↓
Type-aware rules

При создании Program TypeScript:

  • читает конфигурацию;
  • находит все включённые файлы;
  • разрешает импорты;
  • строит граф зависимостей;
  • анализирует декларации типов;
  • подготавливает механизм проверки типов.

На крупных проектах именно этот этап обычно занимает большую часть времени выполнения линтера.


Дополнительные расходы на разрешение модулей

Type-aware анализ требует понимания происхождения каждого типа.

Рассмотрим пример:

import { User } from './models/user';

function process(user: User) {
  return user.name;
}

Чтобы определить тип user.name, TypeScript должен:

  1. Найти файл models/user.ts.
  2. Загрузить его содержимое.
  3. Обработать все его зависимости.
  4. Построить итоговое описание типа User.

Если таких импортов тысячи, объём работы возрастает многократно.

Особенно затратными оказываются:

  • монорепозитории;
  • большие графы зависимостей;
  • многочисленные переэкспорты (export *);
  • сложные path aliases;
  • большие наборы декларационных файлов.

Влияние размера проекта

Стоимость type-aware анализа растёт не линейно относительно количества файлов.

Небольшой проект:

50 файлов
↓
Program создаётся быстро
↓
Проверка занимает секунды

Средний проект:

500 файлов
↓
Большой граф зависимостей
↓
Проверка занимает десятки секунд

Крупный монорепозиторий:

5000+ файлов
↓
Несколько tsconfig
↓
Минуты выполнения

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


Какие правила наиболее затратны

Не все type-aware правила одинаково нагружают систему.

Относительно лёгкие:

await-thenable
no-unnecessary-type-assertion

Средней сложности:

restrict-template-expressions
prefer-nullish-coalescing

Более тяжёлые:

no-misused-promises
no-floating-promises
unbound-method

Причина заключается в количестве обращений к Type Checker и глубине анализа, необходимой для принятия решения.

Например, правило no-floating-promises должно определить:

  • является ли выражение Promise;
  • не происходит ли обработка через await;
  • не используется ли .then();
  • не выполняется ли явное игнорирование результата.

Для этого требуется серия запросов к системе типов.


Повторные обращения к Type Checker

Каждое type-aware правило регулярно запрашивает информацию о типах.

Пример концептуального алгоритма:

const type = checker.getTypeAtLocation(node);

Один такой вызов может инициировать:

  • поиск декларации;
  • вычисление объединённых типов;
  • разрешение обобщений;
  • анализ наследования;
  • вычисление условных типов.

Если правило выполняет тысячи подобных запросов, нагрузка возрастает очень быстро.


Влияние сложных типов

Скорость работы зависит не только от количества файлов, но и от сложности типовой системы проекта.

Простой тип:

type User = {
  id: number;
  name: string;
};

Сложный тип:

type Deep<T> =
  T extends object
    ? {
        [K in keyof T]: Deep<T[K]>;
      }
    : T;

Ещё более тяжёлые конструкции:

type Result<T> =
  T extends Promise<infer U>
    ? U
    : T extends Array<infer V>
      ? V
      : never;

Особенно дорогими считаются:

  • условные типы;
  • рекурсивные типы;
  • deeply nested generics;
  • mapped types;
  • template literal types;
  • distributive conditional types.

Каждый запрос к таким структурам требует дополнительной работы компилятора.


Рост потребления памяти

Type-aware анализ увеличивает не только время выполнения, но и объём используемой памяти.

Причины:

  • хранение AST;
  • хранение Program;
  • хранение графа зависимостей;
  • кэширование типов;
  • кэширование символов;
  • промежуточные результаты Type Checker.

Типичная картина:

Режим Память
Обычный ESLint десятки мегабайт
Type-aware ESLint сотни мегабайт
Крупный монорепозиторий гигабайты

На CI-серверах это может становиться серьёзным ограничением.


Влияние на скорость разработки

Наиболее заметная проблема возникает при локальной разработке.

Разработчик ожидает быстрый цикл:

Изменение файла
↓
Сохранение
↓
Проверка
↓
Результат

Если проверка занимает 20–40 секунд, продуктивность снижается.

Особенно чувствительны:

  • редакторные проверки;
  • pre-commit хуки;
  • watch-режимы;
  • автоматические проверки при сохранении.

Поэтому во многих командах часть type-aware правил запускается только в CI.


Разница между parserOptions.project и projectService

Исторически типизированный анализ строился через настройку:

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

При таком подходе создавался полноценный Program на основе указанного конфигурационного файла.

Современные версии typescript-eslint поддерживают механизм Project Service, который ближе к работе языкового сервиса TypeScript.

Пример настройки:

parserOptions: {
  projectService: true
}

Преимущества:

  • лучшее повторное использование кэшей;
  • снижение количества пересозданий Program;
  • более эффективная работа в редакторах;
  • улучшенная масштабируемость.

На больших проектах разница в производительности может быть весьма заметной.


Кэширование результатов

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

Запуск:

eslint . --cache

Механизм работы:

Первый запуск
↓
Полная проверка
↓
Создание кэша

Следующий запуск
↓
Проверяются только изменённые файлы

Для проектов с type-aware правилами использование кэша практически обязательно, поскольку позволяет существенно сократить время повторных запусков.


Ограничение области анализа

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

Плохой вариант:

{
  "include": ["**/*"]
}

Лучший вариант:

{
  "include": ["src/**/*.ts"]
}

Желательно исключать:

  • тестовые артефакты;
  • сгенерированный код;
  • временные каталоги;
  • build-результаты;
  • сторонние исходники.

Пример:

{
  "exclude": [
    "dist",
    "coverage",
    "generated",
    "node_modules"
  ]
}

Чем меньше файлов участвует в построении Program, тем быстрее работает линтер.


Отдельный tsconfig для ESLint

Распространённая практика — создание специального конфигурационного файла.

Пример:

tsconfig.json
tsconfig.eslint.json

tsconfig.eslint.json:

{
  "extends": "./tsconfig.json",
  "include": [
    "src/**/*.ts"
  ]
}

Такой подход позволяет:

  • исключить лишние файлы;
  • уменьшить размер Program;
  • ускорить запуск линтера;
  • избежать анализа ненужных директорий.

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

Во многих проектах используется двухуровневая стратегия.

Быстрые правила:

eslint src

Запускаются:

  • локально;
  • в редакторе;
  • в pre-commit.

Тяжёлые type-aware правила:

npm run lint:types

Запускаются:

  • в CI;
  • перед релизом;
  • в ночных сборках.

Подобное разделение позволяет сохранить высокий уровень контроля качества без ухудшения пользовательского опыта разработчиков.


Монорепозитории и проблемы масштабирования

В монорепозиториях стоимость type-aware анализа возрастает особенно заметно.

Типичная структура:

packages/
 ├─ api
 ├─ frontend
 ├─ shared
 ├─ ui
 └─ tooling

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

Последствия:

  • длительное построение графа зависимостей;
  • рост потребления памяти;
  • повторная обработка общих библиотек;
  • увеличение времени CI.

Поэтому часто применяются:

  • отдельные tsconfig для пакетов;
  • независимые ESLint-конфигурации;
  • поэтапный запуск линтинга;
  • распределение задач между воркерами.

Профилирование производительности ESLint

Для выявления медленных правил используется специальный режим.

Пример:

TIMING=1 eslint src

Результат показывает время выполнения каждого правила.

Типичный вывод:

Rule                              Time
no-floating-promises            1800ms
no-misused-promises             1400ms
restrict-template-expressions    900ms

Подобная статистика позволяет определить реальные источники замедления и принять решение о необходимости оптимизации.


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

Type-aware правила обеспечивают обнаружение целого класса ошибок, недоступных обычному ESLint:

const result = fetchData();

result.map(x => x.id);

Без информации о типах подобный код может выглядеть корректным.

Type Checker способен определить, что result имеет тип Promise, а не массив, и соответствующее правило сообщит об ошибке.

Однако каждая дополнительная проверка увеличивает:

  • время запуска;
  • объём памяти;
  • нагрузку на CI;
  • стоимость локальной разработки.

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