Профилирование медленных правил

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

CLI-параметр --timing выводит таблицу с распределением времени по правилам. Он фиксирует суммарные затраты на каждое правило в процессе линтинга и позволяет быстро выделить аномалии, где одно правило значительно медленнее остальных.

Типичная картина больших проектов:

  • большинство правил укладываются в доли миллисекунды на файл
  • несколько правил занимают 60–90% всего времени анализа
  • медленные правила часто связаны с глубокими селекторами AST или сложной логикой обхода

Дополнительно используется режим --stats, который показывает общую нагрузку на процесс линтинга: количество файлов, правил, среднее время обработки одного файла. Однако он не даёт детализации по конкретным правилам и служит скорее индикатором деградации производительности.

Архитектура затрат внутри правила

Каждое правило ESLint представляет собой набор обработчиков AST-узлов. Производительность зависит не от количества строк кода в правиле, а от:

  • количества подписок на узлы AST
  • глубины и частоты срабатываний
  • сложности логики внутри обработчика
  • использования селекторов вместо ручного обхода
  • количества вызовов context.report

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

Особенно критичны конструкции:

context.report(node, message);

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

Профилирование через Node.js Profiler

Глубокий анализ возможен через встроенный профилировщик Node.js. ESLint запускается с флагами:

  • --prof
  • --prof-process

Это позволяет получить V8 CPU profile, где видно распределение времени между функциями правил и внутренними механизмами ESLint.

Основные сигналы в профиле:

  • высокая нагрузка в функциях traverse
  • частые вызовы getAncestors
  • использование eslint-utils для сложных проверок
  • дорогие регулярные выражения внутри правил

Визуализация профиля в Chrome DevTools через загрузку .cpuprofile помогает выявить узкие места в AST-обходе.

Частые источники медленных правил

Избыточные селекторы AST

Селекторы вида:

"CallExpression MemberExpression Identifier"

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

Оптимизация заключается в сужении контекста:

  • добавление точечных селекторов
  • проверка node.callee.name перед дальнейшей логикой
  • ранний return при несоответствии условий

Частые проверки типов узлов

Многие правила используют цепочки:

if (node.type === "CallExpression") {
  if (node.callee.type === "MemberExpression") {
    ...
  }
}

При большом количестве узлов это создаёт накопительный overhead. Более эффективно использовать селектор, ограничивающий вход:

CallExpression[callee.type="MemberExpression"]

Регулярные выражения в горячих участках

Регулярные выражения внутри обработчиков AST становятся критичными при больших кодовых базах. Особенно опасны:

  • повторное создание RegExp внутри функции
  • отсутствие кеширования результатов .test()
  • сложные шаблоны с backtracking

Оптимизация заключается в вынесении RegExp в область модуля и сокращении количества проверок.

Инструменты детального анализа правил

ESLint Rule Profiler

Существуют расширения и обёртки, позволяющие фиксировать время выполнения на уровне отдельных правил. Они добавляют измерение:

  • до входа в обработчик
  • после выхода
  • суммарное время по файлу

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

ESLint cache

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

Влияние парсера и AST

Производительность ESLint зависит не только от правил, но и от парсера. При использовании @babel/eslint-parser или @typescript-eslint/parser значительная часть времени уходит на построение AST.

Факторы влияния:

  • глубина дерева (TypeScript генерирует более сложные узлы)
  • наличие типов и деклараций
  • JSX и experimental syntax
  • source maps

В больших проектах парсинг может составлять до 40–60% общего времени линтинга, что делает анализ правил лишь частью общей оптимизации.

Узкие места в context.report

context.report не является дешёвой операцией. При массовых срабатываниях происходит:

  • создание diagnostic-объекта
  • нормализация позиции в файле
  • привязка к source code
  • последующая сериализация

Правила, генерирующие десятки тысяч сообщений на один файл, резко увеличивают время финальной стадии линтинга.

Оптимизация достигается:

  • дедупликацией ошибок
  • ограничением количества сообщений на файл
  • агрегацией проблем вместо точечных репортов

Мониторинг в больших кодовых базах

В монорепозиториях профилирование усложняется из-за неоднородности проектов. Используются подходы:

  • выборочное измерение по пакетам
  • запуск ESLint на подмножестве файлов
  • сравнение распределения времени между директориями
  • сбор статистики по CI-пайплайнам

Метрика “time per file” становится ключевой: при росте кодовой базы важно, чтобы время линтинга росло линейно, а не квадратично.

Влияние конфигурации правил

Конфигурация ESLint напрямую влияет на производительность. Важные факторы:

  • количество активных правил
  • использование plugin-наборов
  • включение тяжёлых правил по умолчанию
  • пересечения между конфигурациями

Некоторые пресеты добавляют десятки правил, часть из которых дублирует проверки. Это создаёт избыточные обходы AST.

Оптимизация конфигурации включает:

  • отключение неиспользуемых правил
  • разделение конфигурации для разных типов файлов
  • применение overrides для тестов и билд-артефактов

Инкрементальное профилирование

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

Используются подходы:

  • baseline-профиль
  • профиль после изменения правила
  • дифф времени выполнения

Особое внимание уделяется регрессиям, когда небольшое изменение в логике правила приводит к экспоненциальному росту времени обработки.

Поведение ESLint в CI

В CI-среде профилирование часто выявляет скрытые проблемы:

  • различие между локальной и CI-файловой системой
  • холодный запуск без кеша
  • параллельные процессы
  • лимиты CPU контейнеров

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

Оптимизация через архитектуру правил

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

  • минимизация количества AST-подписок
  • использование предфильтрации узлов
  • отказ от глубоких рекурсивных обходов внутри правил
  • кеширование промежуточных результатов в WeakMap

Эффективные правила чаще используют точечные селекторы и простую проверку свойств узлов, избегая сложной логики анализа контекста.

Стоимость абстракций и утилит

Использование вспомогательных библиотек (например, для работы с AST) может добавлять накладные расходы. Часто утилиты:

  • создают дополнительные обходы дерева
  • дублируют проверки типов
  • используют избыточные копирования структур

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

Итоговые наблюдения по профилированию

Профилирование ESLint-правил всегда сводится к анализу трёх основных факторов:

  • стоимость обхода AST
  • количество и сложность срабатываний правил
  • стоимость финальной агрегации результатов

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