Как найти правила ESLint, которые замедляют линтинг
Найдите медленные правила ESLint с помощью TIMING=1 и Config Inspector. Сравните типизированные правила, выявите затратные плагины и ускорьте проверку кода.
Если ESLint работает медленно, причина обычно в небольшом числе правил: type-aware правилах typescript-eslint, eslint-plugin-prettier, @stylistic/indent или межфайловых правилах eslint-plugin-import, например import/no-cycle. Найти их можно с помощью TIMING=1 и панели статистики в ESLint Config Inspector.
Медленный линтинг быстро становится дорогим. Pre-commit хук начинает выполняться так долго, что разработчики коммитят с --no-verify, и с этого момента конфигурация линтера уже ничего не защищает.
В этом руководстве разберём, как измерить затраты на каждое правило, какие правила чаще всего оказываются проблемными и что изменить в eslint.config.js для каждого из них.
Главное
- При линтинге с информацией о типах полный прогон ESLint должен занимать примерно столько же времени, сколько
tscна том же проекте. Если прогон идёт намного дольше, проблема в конкретном правиле или в конфигурации. TIMING=1 npx eslint .выводит самые медленные правила. Первое type-aware правило почти всегда выглядит самым медленным, потому что TypeScript кеширует информацию о типах, поэтому проверяйте подозрительные правила по одному.- В ESLint Config Inspector 3.4.0 появились флаг
--statsи панель статистики, которая запускает замер времени ESLint по запросу. Это отдельный флаг, не связанный с собственным--statsESLint. - Документация typescript-eslint рекомендует запускать
import/no-cycle,import/no-unused-modules,import/no-deprecatedиimport/no-named-as-defaultтолько в CI или при push, поскольку у TypeScript нет аналогичной проверки. eslint-plugin-prettierпарсит каждый файл повторно. Отдельный шаг сprettier --checkдаёт тот же результат «прошёл / не прошёл».
Почему ESLint работает медленно?
Обычно ESLint тормозит из-за нескольких ресурсоёмких правил, а не из-за самого TypeScript. При линтинге с информацией о типах полный прогон ESLint должен занимать примерно столько же времени, сколько проверка типов. Согласно руководству typescript-eslint по производительности, замедление обычно вызывают type-aware правила, запрашивающие информацию у type checker TypeScript. Сначала измерьте базовое время:
npx tsc --noEmit
Если tsc отрабатывает быстро, а линтинг заметно медленнее, виновато какое-то правило или настройка конфигурации, а не TypeScript.
Как проверить время работы правил с помощью TIMING=1?
Если задать переменную окружения TIMING, ESLint выведет правила, на которые ушло больше всего времени. Ничего устанавливать не нужно:
TIMING=1 npx eslint .
Это поведение описано в разделе документации ESLint Profile Rule Performance.
Интерпретируйте вывод с осторожностью. TypeScript строит информацию о типах, когда её запрашивает первое type-aware правило, и кеширует её для всех последующих. Поэтому первое type-aware правило в прогоне почти всегда выглядит самым медленным, даже если на деле оно дешёвое.
Для честного сравнения замеряйте каждое type-aware правило в отдельном прогоне, отключив остальные. Один из способов — держать урезанный конфиг с единственным подозреваемым правилом и указывать его ESLint через -c/--config:
TIMING=1 npx eslint -c eslint.timing.config.js .
Просмотр времени в ESLint Config Inspector
В ESLint Config Inspector есть представление с замерами времени. Согласно примечаниям к релизам, в версии 3.4.0 у команд dev и build появился флаг --stats, а также панель статистики, запускающая замер времени ESLint по запросу. Таким образом, для этого представления нужен Config Inspector 3.4 или новее. Используйте последнюю версию 3.x: в 3.4.1 исправлено падение build --stats, возникавшее, когда ESLint сообщал о прогоне без данных о времени парсинга (issue #333).
Этот --stats относится к Config Inspector. У ESLint есть собственный флаг --stats, который добавляет данные о времени в результат линтинга каждого файла, чтобы их мог вывести форматтер. Никакой панели он не открывает.
Выполните эти команды в каталоге, где находится ваш eslint.config.js:
# List the options available in your installed version
pnpx @eslint/config-inspector --help
# Static snapshot with timing data, written to dist/__eslint-config-inspector
pnpx @eslint/config-inspector build --stats
Простой запуск pnpx @eslint/config-inspector открывает инспектор по адресу http://localhost:7777. Передайте --stats команде dev (точный синтаксис покажет --help), откройте панель статистики и найдите правила и плагины, на которые приходится основная часть времени. Оговорка про первое type-aware правило действует и здесь. Статическая сборка — это снимок, поэтому можно сохранить снимок до изменения конфигурации и сравнить его со снимком после.
Какие правила ESLint обычно самые медленные?
В верхней части вывода с замерами раз за разом оказываются одни и те же четыре группы правил:
- Type-aware правила. Пресеты вроде
recommendedTypeCheckedдобавляют правила, зависящие от type checker. Настройка описана в статье линтинг TypeScript с помощью ESLint. eslint-plugin-prettier. Плагин может обнаружить расхождение в форматировании, только прогнав Prettier по всему файлу, поэтому каждый проверяемый файл парсится сначала ESLint, а затем ещё раз Prettier. В статье ESLint и Prettier объясняется, как эти инструменты разделяют обязанности.@stylistic/indent. Правило вычисляет отступы для каждого токена в каждом файле, и в большой кодовой базе эта работа накапливается.- Правила
eslint-plugin-import, особенноimport/no-cycle. Эти правила заставляют плагин самостоятельно парсить файлы и отслеживать импорты, независимо от ESLint, поэтому затраты растут вместе с размером кодовой базы. О том, что отлавливает этот плагин, читайте в статье плагины ESLint, улучшающие качество кода.
Как исправить каждое медленное правило
Для каждой группы медленных правил есть своё решение. Большинство рекомендаций ниже взяты из руководства typescript-eslint по производительности:
| Виновник | Почему медленно | Решение |
|---|---|---|
| Type-aware правила | Обращаются к type checker | Ограничьте их исходниками через files и сузьте include в tsconfig |
eslint-plugin-prettier | Повторно парсит каждый файл | Удалите его и запускайте prettier --check . |
@stylistic/indent | Обрабатывает каждый токен | Удалите его и доверьте отступы Prettier |
import/named, import/namespace, import/default, import/no-named-as-default-member, import/no-unresolved | Дублируют проверки TypeScript | Отключите их (no-unresolved — только если в коде используется import, а не require) |
import/no-cycle, import/no-unused-modules, import/no-deprecated, import/no-named-as-default | Межфайловый анализ без аналога в TypeScript | Запускайте их только в CI или при push |
Проверьте и tsconfig. Если в tsconfig нет include или указан glob вроде **/*, парсер заранее разбирает гораздо больше файлов, чем вы собирались проверять, включая результаты сборки. Ограничьте include каталогами, которые действительно нужно линтить:
{ "compilerOptions": { /* ... */ }, "include": ["src"] }
Вместо плагина используйте --check в Prettier как отдельный шаг в CI. Для повторных локальных запусков флаг ESLint --cache пропускает файлы, не изменившиеся с прошлого прогона.
Пример: до и после
Исходный конфиг ниже применяет всё ко всем файлам:
// @ts-check
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';
import stylistic from '@stylistic/eslint-plugin';
import prettierPlugin from 'eslint-plugin-prettier';
export default defineConfig({
files: ['**/*.{js,ts}'],
extends: [tseslint.configs.recommendedTypeChecked],
languageOptions: {
parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
},
plugins: { import: importPlugin, '@stylistic': stylistic, prettier: prettierPlugin },
rules: {
'prettier/prettier': 'error',
'@stylistic/indent': ['error', 2],
'import/named': 'error',
'import/no-unresolved': 'error',
'import/no-cycle': 'error',
},
});
Итоговый конфиг ограничивает type-aware правила каталогом src, убирает плагины форматирования и правила импорта, которые уже покрывает TypeScript, и включает import/no-cycle только при заданной переменной окружения:
// @ts-check
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';
const full = process.env.LINT_FULL === '1';
export default defineConfig(
{
files: ['src/**/*.ts'],
extends: [tseslint.configs.recommendedTypeChecked],
languageOptions: {
parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
},
},
{
files: ['src/**/*.ts'],
plugins: { import: importPlugin },
rules: {
...(full ? { 'import/no-cycle': 'error' } : {}),
},
},
);
С таким конфигом pre-commit хук запускает npx eslint ., а CI — LINT_FULL=1 npx eslint . и npx prettier --check ..
Таблица ниже — иллюстративный пример того, как меняется список TIMING=1 после такого изменения конфигурации. Это не результаты реальных измерений:
| Правило | До | После |
|---|---|---|
prettier/prettier | В верхней части | Отсутствует (заменено на prettier --check) |
@stylistic/indent | В верхней части | Отсутствует |
import/no-cycle | В верхней части | Появляется только в прогонах с LINT_FULL=1 |
import/named, import/no-unresolved | В списке | Отсутствуют |
| Первое type-aware правило | Отображается как самое медленное | По-прежнему первое; сравните со временем tsc |
Заключение
Прежде чем что-то удалять, проведите измерения. Начните с базового времени tsc, затем запустите TIMING=1 и панель статистики Config Inspector и замерьте подозрительные type-aware правила по одному. Выяснив, какие правила обходятся дороже всего, оставьте для локального и pre-commit линтинга быстрые однофайловые правила, а межфайловые проверки и форматирование перенесите в CI. Запустите TIMING=1 npx eslint . в своём проекте прямо сейчас и пройдитесь по таблице решений для правил, оказавшихся в верхней части списка.
Часто задаваемые вопросы
Как увидеть больше 10 правил в выводе TIMING в ESLint?
Задайте TIMING большее число или значение all. TIMING=1 показывает десять самых долгих правил, TIMING=50 — пятьдесят, а TIMING=all — все правила. В каждой строке указано время правила в миллисекундах (создание правила плюс выполнение) и его доля в общем времени работы правил. Чтобы замерить одно правило, комбинируйте --no-config-lookup с --rule. Это подходит для встроенных правил, которым не нужна настройка парсера.
Безопасно ли использовать --cache в ESLint с type-aware правилами?
Не полностью. Кеш ESLint учитывает только то, изменились ли файл или конфигурация. Изменения в других файлах, от которых зависит данный файл, он игнорирует. Поэтому type-aware правила typescript-eslint и правила eslint-plugin-import, разрешающие импорты, могут выдавать устаревшие результаты в обе стороны: пропускать новую проблему или продолжать сообщать об уже исправленной. В FAQ typescript-eslint --cache не рекомендуется, поэтому запускайте CI без него.
Как получить в ESLint время по каждому файлу и каждому правилу?
Запустите ESLint с его собственным флагом --stats и форматтером, который выводит данные статистики, например npx eslint --stats -f json. Каждый объект результата получает ключ stats с временем парсинга, временем исправлений и временем линтинга по каждому правилу для этого файла, тогда как TIMING лишь подводит итог по всему прогону. Не каждый форматтер выводит данные статистики. Оговорка о кешировании для первого type-aware правила действует и для --stats.
Делает ли parserOptions.projectService линтинг с типами быстрее, чем parserOptions.project?
Не гарантированно. В typescript-eslint ожидали, что Project Service, доступный начиная с v8.0.0, будет быстрее, и рассматривают отсутствие прироста скорости как баг. Главное преимущество — более простая конфигурация: больше не нужны glob-шаблоны в parserOptions.project, где рекурсивные шаблоны ** вызывают лишние операции дискового ввода-вывода. Держите allowDefaultProject небольшим: каждый подпадающий под него файл заметно замедляет линтинг, а по умолчанию он может охватывать не более 8 файлов.