12k
All articles

Como encontrar as regras do ESLint que deixam o seu lint lento

Identifique regras lentas do ESLint com TIMING=1 e Config Inspector. Compare regras com tipos, encontre plugins custosos e ajuste a configuração para acelerar o lint.

OpenReplay Team
OpenReplay Team
Como encontrar as regras do ESLint que deixam o seu lint lento

Quando o ESLint está lento, a causa costuma ser um pequeno número de regras: regras do typescript-eslint que usam informações de tipo, eslint-plugin-prettier, @stylistic/indent ou regras do eslint-plugin-import que analisam vários arquivos, como import/no-cycle. Você pode identificá-las com TIMING=1 e com o painel de estatísticas do ESLint Config Inspector.

Um lint lento rapidamente sai caro. O hook de pre-commit passa a demorar tanto que as pessoas começam a fazer commit com --no-verify, e a partir daí a configuração de lint deixa de proteger qualquer coisa.

Este guia mostra como medir o custo de cada regra, quais regras costumam ser o problema e o que mudar no eslint.config.js em cada caso.

Principais conclusões

  • Com linting baseado em tipos, uma execução completa do ESLint deve levar mais ou menos o mesmo tempo que o tsc no mesmo projeto. Uma execução muito mais demorada indica um problema específico de regra ou de configuração.
  • TIMING=1 npx eslint . exibe as regras mais lentas. A primeira regra baseada em tipos quase sempre parece a mais lenta, porque o TypeScript armazena as informações de tipo em cache; por isso, teste as suspeitas uma de cada vez.
  • O ESLint Config Inspector 3.4.0 adicionou a flag --stats e um painel de estatísticas que executa a medição de tempo do ESLint sob demanda. Essa flag é diferente da --stats do próprio ESLint.
  • A documentação do typescript-eslint recomenda executar import/no-cycle, import/no-unused-modules, import/no-deprecated e import/no-named-as-default somente no CI ou no momento do push, porque o TypeScript não tem uma verificação equivalente.
  • O eslint-plugin-prettier faz o parsing de cada arquivo uma segunda vez. Executar prettier --check como uma etapa separada produz o mesmo resultado de aprovação ou reprovação.

Por que o ESLint é lento?

Normalmente o ESLint fica lento por causa de algumas regras caras, e não por causa do TypeScript em si. Com linting baseado em tipos, uma execução completa do ESLint deve levar aproximadamente o mesmo tempo que a checagem de tipos. Segundo o guia de desempenho do typescript-eslint, a causa mais comum de lentidão são as regras baseadas em tipos solicitando informações ao type checker do TypeScript. Meça primeiro a sua linha de base:

npx tsc --noEmit

Se o tsc é rápido e o lint é muito mais lento, o culpado é uma regra ou uma configuração, não o TypeScript.

Como verificar o tempo das regras com TIMING=1?

Definir a variável de ambiente TIMING faz o ESLint exibir as regras que mais consumiram tempo. Não é preciso instalar nada:

TIMING=1 npx eslint .

Esse comportamento está descrito na documentação Profile Rule Performance do ESLint.

Tenha cuidado ao interpretar a saída. O TypeScript monta as informações de tipo quando a primeira regra baseada em tipos as solicita e as mantém em cache para todas as regras seguintes. Por isso, a primeira regra baseada em tipos da execução quase sempre parece a mais lenta, mesmo quando é barata.

Para uma comparação justa, meça cada regra baseada em tipos em uma execução separada, com as demais desativadas. Uma forma de fazer isso é manter uma configuração enxuta que contenha uma única suspeita e apontar o ESLint para ela com -c/--config:

TIMING=1 npx eslint -c eslint.timing.config.js .

Veja os tempos no ESLint Config Inspector

O ESLint Config Inspector inclui uma visualização de tempos. De acordo com as notas de versão, a versão 3.4.0 adicionou a flag --stats aos comandos dev e build, além de um painel de estatísticas que executa a medição de tempo do ESLint sob demanda. Portanto, a visualização de tempos exige o Config Inspector 3.4 ou posterior. Use a versão 3.x mais recente, porque a 3.4.1 corrigiu uma falha no build --stats que ocorria quando o ESLint reportava uma passagem sem tempo de parsing (issue #333).

Essa --stats pertence ao Config Inspector. O ESLint tem sua própria flag --stats, que inclui os tempos no resultado do lint de cada arquivo para que um formatter os exiba. Ela não abre nenhum painel.

Execute os comandos abaixo na pasta que contém o seu 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

Executar apenas pnpx @eslint/config-inspector inicia o inspector em http://localhost:7777. Passe --stats para o comando dev (o --help mostra a forma exata), abra o painel de estatísticas e procure as regras e plugins responsáveis pela maior parte do tempo. A ressalva sobre a primeira regra baseada em tipos também vale nessa visualização. O build estático é um snapshot, então você pode guardar um feito antes de uma mudança na configuração e compará-lo com outro feito depois.

Quais regras do ESLint costumam ser as mais lentas?

Os mesmos quatro grupos de regras do ESLint aparecem repetidamente no topo da saída de tempos:

  • Regras baseadas em tipos. Presets como recommendedTypeChecked adicionam regras que dependem do type checker. A configuração é abordada em linting de TypeScript com ESLint.
  • eslint-plugin-prettier. O plugin só consegue detectar uma diferença de formatação executando o Prettier sobre o arquivo inteiro, então cada arquivo analisado passa por um parsing do ESLint e outro do Prettier. O artigo ESLint e Prettier explica como as duas ferramentas dividem o trabalho.
  • @stylistic/indent. Ela calcula a indentação de cada token em cada arquivo, e em uma base de código grande esse trabalho se acumula.
  • Regras do eslint-plugin-import, especialmente import/no-cycle. Essas regras fazem o plugin realizar o parsing dos arquivos e seguir os imports por conta própria, separadamente do ESLint, de modo que o custo cresce com o tamanho da base de código. Veja plugins do ESLint que melhoram a qualidade do código para saber o que o plugin detecta.

Como corrigir cada regra lenta

Cada grupo de regras lentas do ESLint tem sua própria correção. A maioria das recomendações abaixo vem do guia de desempenho do typescript-eslint:

ResponsávelPor que é lentoCorreção
Regras baseadas em tiposFazem chamadas ao type checkerRestrinja-as aos arquivos de código-fonte com files e limite o include do tsconfig
eslint-plugin-prettierFaz o parsing de cada arquivo uma segunda vezRemova-o e execute prettier --check .
@stylistic/indentProcessa todos os tokensRemova-a e deixe a indentação a cargo do Prettier
import/named, import/namespace, import/default, import/no-named-as-default-member, import/no-unresolvedDuplicam verificações do TypeScriptDesative-as (no-unresolved somente se o código usar import, e não require)
import/no-cycle, import/no-unused-modules, import/no-deprecated, import/no-named-as-defaultAnálise entre arquivos sem equivalente no TypeScriptExecute-as apenas no CI ou no momento do push

Verifique também o tsconfig. Um tsconfig sem include, ou com um glob como **/*, faz o parser pré-processar muito mais arquivos do que você pretendia analisar, incluindo a saída do build. Limite o include às pastas que você realmente quer analisar:

{ "compilerOptions": { /* ... */ }, "include": ["src"] }

Use o --check do Prettier como uma etapa separada no CI em vez do plugin. Para execuções locais repetidas, a flag --cache do ESLint ignora arquivos que não mudaram desde a última execução.

Exemplo: antes e depois

A configuração “antes”, abaixo, executa tudo em todos os arquivos:

// @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',
  },
});

A configuração “depois” restringe as regras baseadas em tipos a src, remove os plugins de formatação e as regras de import que o TypeScript já cobre, e ativa import/no-cycle somente quando uma variável de ambiente está definida:

// @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' } : {}),
    },
  },
);

Com essa configuração, o hook de pre-commit executa npx eslint ., enquanto o CI executa LINT_FULL=1 npx eslint . e npx prettier --check ..

A tabela abaixo é um exemplo ilustrativo de como a lista do TIMING=1 muda após essa alteração na configuração. Não se trata de resultados medidos:

RegraAntesDepois
prettier/prettierPerto do topoRemovida (substituída por prettier --check)
@stylistic/indentPerto do topoRemovida
import/no-cyclePerto do topoAparece apenas nas execuções com LINT_FULL=1
import/named, import/no-unresolvedListadasRemovidas
Primeira regra baseada em tiposListada como a mais lentaContinua em primeiro; compare com o tempo do tsc

Conclusão

Meça antes de remover qualquer coisa. Comece com uma linha de base do tsc, depois execute TIMING=1 e o painel de estatísticas do Config Inspector, e meça as regras baseadas em tipos suspeitas uma de cada vez. Depois de saber quais regras custam mais, mantenha no lint local e no pre-commit apenas regras rápidas, que analisam um arquivo por vez, e mova as verificações entre arquivos e a formatação para o CI. Execute TIMING=1 npx eslint . no seu projeto agora e siga a tabela de correções para as regras que aparecerem no topo.

Perguntas frequentes

Como vejo mais de 10 regras na saída do TIMING do ESLint?

Defina TIMING com um número maior ou com all. TIMING=1 mostra as dez regras mais demoradas, TIMING=50 mostra cinquenta e TIMING=all mostra todas as regras. Cada linha mostra o tempo da regra em milissegundos (criação da regra mais execução) e sua participação no tempo total das regras. Para medir uma única regra, combine --no-config-lookup com --rule. Isso funciona bem para regras nativas que não precisam de configuração de parser.

É seguro usar o --cache do ESLint com regras baseadas em tipos?

Não totalmente. O cache do ESLint verifica apenas se um arquivo ou a configuração mudou. Ele ignora mudanças nos outros arquivos dos quais um arquivo depende. Por isso, regras do typescript-eslint baseadas em tipos e regras do eslint-plugin-import que resolvem imports podem reportar resultados desatualizados em qualquer direção: podem deixar passar um problema novo ou continuar reportando um que já foi corrigido. O FAQ do typescript-eslint não recomenda o --cache, então execute o CI sem ele.

Como obtenho tempos por arquivo e por regra no ESLint?

Execute o ESLint com a flag --stats dele próprio e um formatter que exiba dados de estatísticas, por exemplo npx eslint --stats -f json. Cada objeto de resultado recebe uma chave stats com os tempos de parsing, os tempos de correção e os tempos de lint por regra daquele arquivo, enquanto o TIMING apenas resume a execução inteira. Nem todo formatter exibe dados de estatísticas. A ressalva sobre o cache da primeira regra baseada em tipos também se aplica ao --stats.

O parserOptions.projectService torna o linting com tipos mais rápido que o parserOptions.project?

Não de forma confiável. O typescript-eslint esperava que o Project Service, disponível a partir da v8.0.0, fosse mais rápido, e afirma que está tratando a ausência desse ganho de velocidade como um bug. O principal benefício é uma configuração mais simples: você não precisa mais de globs em parserOptions.project, nos quais padrões recursivos ** geram I/O extra em disco. Mantenha o allowDefaultProject pequeno, porque cada arquivo que ele abrange deixa o linting visivelmente mais lento, e por padrão ele pode abranger no máximo 8 arquivos.

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.