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.
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
tscno 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
--statse um painel de estatísticas que executa a medição de tempo do ESLint sob demanda. Essa flag é diferente da--statsdo próprio ESLint. - A documentação do typescript-eslint recomenda executar
import/no-cycle,import/no-unused-modules,import/no-deprecatedeimport/no-named-as-defaultsomente no CI ou no momento do push, porque o TypeScript não tem uma verificação equivalente. - O
eslint-plugin-prettierfaz o parsing de cada arquivo uma segunda vez. Executarprettier --checkcomo 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
recommendedTypeCheckedadicionam 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, especialmenteimport/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ável | Por que é lento | Correção |
|---|---|---|
| Regras baseadas em tipos | Fazem chamadas ao type checker | Restrinja-as aos arquivos de código-fonte com files e limite o include do tsconfig |
eslint-plugin-prettier | Faz o parsing de cada arquivo uma segunda vez | Remova-o e execute prettier --check . |
@stylistic/indent | Processa todos os tokens | Remova-a e deixe a indentação a cargo do Prettier |
import/named, import/namespace, import/default, import/no-named-as-default-member, import/no-unresolved | Duplicam verificações do TypeScript | Desative-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-default | Análise entre arquivos sem equivalente no TypeScript | Execute-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:
| Regra | Antes | Depois |
|---|---|---|
prettier/prettier | Perto do topo | Removida (substituída por prettier --check) |
@stylistic/indent | Perto do topo | Removida |
import/no-cycle | Perto do topo | Aparece apenas nas execuções com LINT_FULL=1 |
import/named, import/no-unresolved | Listadas | Removidas |
| Primeira regra baseada em tipos | Listada como a mais lenta | Continua 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.