12k
All articles

Identifier les règles ESLint qui ralentissent votre linting

Repérez les règles ESLint lentes avec TIMING=1 et Config Inspector. Comparez les règles typées, ciblez les plugins coûteux et optimisez votre configuration.

OpenReplay Team
OpenReplay Team
Identifier les règles ESLint qui ralentissent votre linting

Lorsqu’ESLint est lent, la cause se résume généralement à un petit nombre de règles : les règles typées (type-aware) de typescript-eslint, eslint-plugin-prettier, @stylistic/indent ou les règles inter-fichiers d’eslint-plugin-import, comme import/no-cycle. Vous pouvez les repérer grâce à TIMING=1 et au panneau de statistiques d’ESLint Config Inspector.

Un linting lent devient vite coûteux. Le hook de pre-commit prend tellement de temps que les développeurs se mettent à committer avec --no-verify, et à partir de là, la configuration de lint ne protège plus rien.

Ce guide explique comment mesurer le coût de chaque règle, quelles règles posent généralement problème et quelles modifications apporter à eslint.config.js dans chaque cas.

Points clés

  • Avec le linting typé, une exécution complète d’ESLint devrait prendre à peu près autant de temps que tsc sur le même projet. Une exécution nettement plus longue révèle un problème lié à une règle précise ou à la configuration.
  • TIMING=1 npx eslint . affiche les règles les plus lentes. La première règle typée paraît presque toujours la plus lente, car TypeScript met en cache les informations de type : testez donc les suspectes une par une.
  • ESLint Config Inspector 3.4.0 a ajouté une option --stats et un panneau de statistiques qui lance le chronométrage d’ESLint à la demande. Cette option est distincte de l’option --stats propre à ESLint.
  • La documentation de typescript-eslint recommande de n’exécuter import/no-cycle, import/no-unused-modules, import/no-deprecated et import/no-named-as-default qu’en CI ou au moment du push, car TypeScript ne propose aucune vérification équivalente.
  • eslint-plugin-prettier analyse chaque fichier une seconde fois. Exécuter prettier --check comme étape à part entière donne le même résultat (succès ou échec).

Pourquoi ESLint est-il lent ?

La lenteur d’ESLint provient généralement de quelques règles coûteuses, et non de TypeScript lui-même. Avec le linting typé, une exécution complète d’ESLint devrait durer à peu près autant que la vérification de types. Selon le guide de performance de typescript-eslint, un ralentissement s’explique le plus souvent par des règles typées qui sollicitent le vérificateur de types de TypeScript. Commencez par mesurer votre référence :

npx tsc --noEmit

Si tsc est rapide et que le lint est beaucoup plus lent, c’est une règle ou un paramètre de configuration qui est en cause, et non TypeScript.

Comment vérifier le temps d’exécution des règles avec TIMING=1 ?

Définir la variable d’environnement TIMING amène ESLint à afficher les règles qui ont pris le plus de temps. Aucune installation n’est nécessaire :

TIMING=1 npx eslint .

Ce comportement est décrit dans la documentation d’ESLint, section Profile Rule Performance.

Interprétez la sortie avec prudence. TypeScript construit les informations de type lorsque la première règle typée les demande, puis les met en cache pour toutes les règles suivantes. Par conséquent, la première règle typée de l’exécution paraît presque toujours la plus lente, même lorsqu’elle est peu coûteuse.

Pour une comparaison équitable, chronométrez chaque règle typée lors d’une exécution distincte, en désactivant les autres. Une solution consiste à conserver une configuration allégée ne contenant qu’une seule règle suspecte et à la transmettre à ESLint via -c/--config :

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

Visualiser les temps d’exécution dans ESLint Config Inspector

ESLint Config Inspector propose une vue de chronométrage. D’après ses notes de version, la version 3.4.0 a doté les commandes dev et build d’une option --stats et ajouté un panneau de statistiques qui lance le chronométrage d’ESLint à la demande. La vue de chronométrage nécessite donc Config Inspector 3.4 ou une version ultérieure. Utilisez la version 3.x la plus récente, car la 3.4.1 a corrigé un plantage de build --stats qui survenait lorsqu’ESLint renvoyait un passage sans temps d’analyse syntaxique (issue #333).

Cette option --stats appartient à Config Inspector. ESLint possède sa propre option --stats, qui ajoute des données de chronométrage au résultat de lint de chaque fichier afin qu’un formateur puisse les afficher. Elle n’ouvre aucun panneau.

Exécutez ces commandes dans le dossier qui contient votre 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

La simple commande pnpx @eslint/config-inspector lance l’inspecteur sur http://localhost:7777. Passez --stats à la commande dev (--help indique la syntaxe exacte), ouvrez le panneau de statistiques et repérez les règles et plugins qui concentrent l’essentiel du temps. La mise en garde concernant la première règle typée s’applique également à cette vue. Le build statique étant un instantané, vous pouvez en conserver un avant une modification de configuration et le comparer à un autre réalisé après.

Quelles règles ESLint sont généralement les plus lentes ?

Les quatre mêmes groupes de règles ESLint reviennent sans cesse en tête des résultats de chronométrage :

  • Les règles typées. Les presets tels que recommendedTypeChecked ajoutent des règles qui dépendent du vérificateur de types. Leur mise en place est détaillée dans l’article linter du TypeScript avec ESLint.
  • eslint-plugin-prettier. Le plugin ne peut détecter un écart de formatage qu’en exécutant Prettier sur l’ensemble du fichier : chaque fichier linté est donc analysé une première fois par ESLint, puis une seconde fois par Prettier. L’article ESLint et Prettier explique comment les deux outils se répartissent le travail.
  • @stylistic/indent. Cette règle calcule l’indentation de chaque token dans chaque fichier, et sur une base de code volumineuse, ce travail s’accumule.
  • Les règles d’eslint-plugin-import, en particulier import/no-cycle. Ces règles obligent le plugin à analyser les fichiers et à suivre les imports lui-même, indépendamment d’ESLint, si bien que leur coût augmente avec la taille de la base de code. Consultez les plugins ESLint qui améliorent la qualité du code pour découvrir ce que détecte ce plugin.

Comment corriger chaque règle lente

Chaque groupe de règles ESLint lentes a sa propre solution. La plupart des recommandations ci-dessous proviennent du guide de performance de typescript-eslint :

CoupableCause de la lenteurSolution
Règles typéesFont appel au vérificateur de typesLes limiter aux fichiers source via files et restreindre le include du tsconfig
eslint-plugin-prettierAnalyse chaque fichier une seconde foisLe supprimer et exécuter prettier --check .
@stylistic/indentTraite chaque tokenLa supprimer et laisser Prettier gérer l’indentation
import/named, import/namespace, import/default, import/no-named-as-default-member, import/no-unresolvedFont doublon avec les vérifications de TypeScriptLes désactiver (no-unresolved uniquement si le code utilise import et non require)
import/no-cycle, import/no-unused-modules, import/no-deprecated, import/no-named-as-defaultAnalyse inter-fichiers sans équivalent TypeScriptNe les exécuter qu’en CI ou au moment du push

Vérifiez également le tsconfig. Un tsconfig sans include, ou avec un glob tel que **/*, amène le parser à pré-analyser bien plus de fichiers que vous ne souhaitiez linter, y compris les fichiers de build. Limitez include aux dossiers que vous voulez réellement linter :

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

Utilisez l’option --check de Prettier comme étape de CI distincte, à la place du plugin. Pour les exécutions locales répétées, l’option --cache d’ESLint ignore les fichiers qui n’ont pas changé depuis la dernière exécution.

Exemple : avant et après

La configuration « avant » ci-dessous exécute tout sur chaque fichier :

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

La configuration « après » limite les règles typées à src, supprime les plugins de formatage ainsi que les règles d’import déjà couvertes par TypeScript, et n’active import/no-cycle que lorsqu’une variable d’environnement est définie :

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

Avec cette configuration, le hook de pre-commit exécute npx eslint ., tandis que la CI exécute LINT_FULL=1 npx eslint . et npx prettier --check ..

Le tableau ci-dessous illustre, à titre d’exemple, l’évolution de la liste TIMING=1 après ce changement de configuration. Il ne s’agit pas de résultats mesurés :

RègleAvantAprès
prettier/prettierEn tête de listeDisparue (remplacée par prettier --check)
@stylistic/indentEn tête de listeDisparue
import/no-cycleEn tête de listeN’apparaît que lors des exécutions LINT_FULL=1
import/named, import/no-unresolvedPrésentesDisparues
Première règle typéeIndiquée comme la plus lenteToujours en tête ; à comparer avec le temps de tsc

Conclusion

Mesurez avant de supprimer quoi que ce soit. Commencez par établir une référence avec tsc, puis exécutez TIMING=1 et le panneau de statistiques de Config Inspector, et chronométrez les règles typées suspectes une par une. Une fois les règles les plus coûteuses identifiées, réservez le linting local et de pre-commit aux règles rapides portant sur un seul fichier, et déplacez les vérifications inter-fichiers et le formatage vers la CI. Lancez dès maintenant TIMING=1 npx eslint . sur votre projet et appliquez le tableau de solutions aux règles qui arrivent en tête.

FAQ

Comment afficher plus de 10 règles dans la sortie TIMING d'ESLint ?

Attribuez à TIMING un nombre plus élevé ou la valeur all. TIMING=1 affiche les dix règles les plus longues à s'exécuter, TIMING=50 en affiche cinquante et TIMING=all les affiche toutes. Chaque ligne indique le temps de la règle en millisecondes (création de la règle plus exécution) et sa part dans le temps total consacré aux règles. Pour chronométrer une seule règle, combinez --no-config-lookup avec --rule. Cette méthode convient aux règles natives qui ne nécessitent aucune configuration de parser.

L'option --cache d'ESLint est-elle fiable avec les règles typées ?

Pas entièrement. Le cache d'ESLint vérifie uniquement si un fichier ou la configuration a changé. Il ignore les modifications apportées aux autres fichiers dont dépend un fichier. Les règles typées de typescript-eslint et les règles de résolution d'imports d'eslint-plugin-import peuvent donc renvoyer des résultats obsolètes dans les deux sens : elles peuvent manquer un nouveau problème ou continuer à signaler un problème déjà corrigé. La FAQ de typescript-eslint déconseille --cache : exécutez donc la CI sans cette option.

Comment obtenir d'ESLint des temps d'exécution par fichier et par règle ?

Exécutez ESLint avec sa propre option --stats et un formateur qui restitue les données de statistiques, par exemple npx eslint --stats -f json. Chaque objet de résultat reçoit une clé stats contenant les temps d'analyse syntaxique, les temps de correction et les temps de lint par règle pour ce fichier, alors que TIMING se contente de résumer l'exécution globale. Tous les formateurs ne restituent pas les données de statistiques. La mise en garde concernant la mise en cache par la première règle typée s'applique également à --stats.

parserOptions.projectService rend-il le linting typé plus rapide que parserOptions.project ?

Pas de manière fiable. typescript-eslint s'attendait à ce que le Project Service, disponible depuis la v8.0.0, soit plus rapide, et indique traiter l'absence de gain de vitesse comme un bug. Son principal avantage réside dans une configuration simplifiée : vous n'avez plus besoin des globs de parserOptions.project, dans lesquels les motifs récursifs ** génèrent des E/S disque supplémentaires. Limitez au maximum allowDefaultProject, car chaque fichier qu'il cible ralentit sensiblement le linting ; par défaut, il ne peut cibler que 8 fichiers au plus.

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.