12k
All articles

Как найти правила ESLint, которые замедляют линтинг

Найдите медленные правила ESLint с помощью TIMING=1 и Config Inspector. Сравните типизированные правила, выявите затратные плагины и ускорьте проверку кода.

OpenReplay Team
OpenReplay Team
Как найти правила ESLint, которые замедляют линтинг

Если 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 по запросу. Это отдельный флаг, не связанный с собственным --stats ESLint.
  • Документация 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 файлов.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.