12k
All articles

找出拖慢 ESLint 检查速度的规则

使用TIMING=1和Config Inspector找出耗时的ESLint规则。比较类型感知规则、定位高开销插件,并调整配置以加快代码检查。

OpenReplay Team
OpenReplay Team
找出拖慢 ESLint 检查速度的规则

ESLint 运行缓慢时,原因通常只是少数几条规则:类型感知(type-aware)的 typescript-eslint 规则、eslint-plugin-prettier、@stylistic/indent,或者 import/no-cycle 这类跨文件的 eslint-plugin-import 规则。你可以借助 TIMING=1 和 ESLint Config Inspector 的统计面板把它们找出来。

lint 一慢,代价很快就会上升。pre-commit 钩子耗时太长,大家就会开始用 --no-verify 提交,到那时 lint 配置就形同虚设了。

本文介绍如何衡量每条规则的开销、哪些规则通常是罪魁祸首,以及针对每条规则应如何修改 eslint.config.js。

核心要点

  • 启用类型感知 lint 时,一次完整的 ESLint 运行耗时应与在同一项目上运行 tsc 大致相当。如果耗时明显更长,说明存在特定的规则或配置问题。
  • TIMING=1 npx eslint . 会打印最慢的规则。由于 TypeScript 会缓存类型信息,第一条类型感知规则几乎总是显得最慢,因此应逐一测试可疑规则。
  • ESLint Config Inspector 3.4.0 新增了 --stats 标志和统计面板,可按需运行 ESLint 计时。它与 ESLint 自身的 --stats 是两个不同的标志。
  • typescript-eslint 文档建议仅在 CI 或 push 阶段运行 import/no-cycle、import/no-unused-modules、import/no-deprecated 和 import/no-named-as-default,因为 TypeScript 没有对应的检查。
  • eslint-plugin-prettier 会对每个文件进行第二次解析。将 prettier --check 作为独立步骤运行,可以得到相同的通过/失败结果。

ESLint 为什么慢?

ESLint 变慢通常是因为少数几条开销大的规则,而不是 TypeScript 本身。启用类型感知 lint 时,一次完整的 ESLint 运行耗时应与类型检查大致相当。根据 typescript-eslint 性能指南,变慢的常见原因是类型感知规则向 TypeScript 类型检查器请求信息。首先测量基准耗时:

npx tsc --noEmit

如果 tsc 很快而 lint 慢得多,那么问题出在某条规则或某项配置上,而不是 TypeScript 本身。

如何用 TIMING=1 查看规则耗时?

设置 TIMING 环境变量后,ESLint 会打印耗时最多的规则,无需安装任何东西:

TIMING=1 npx eslint .

该行为在 ESLint 的 Profile Rule Performance 文档中有说明。

解读输出时要小心。TypeScript 会在第一条类型感知规则请求类型信息时构建这些信息,并为后续所有规则缓存。因此,本次运行中的第一条类型感知规则几乎总是显得最慢,即便它的开销其实很低。

为了公平比较,应让每条类型感知规则单独运行一次并计时,同时关闭其他规则。一种做法是保留一份精简配置,其中只包含一条可疑规则,然后通过 -c/--config 让 ESLint 使用它:

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 标志,它会把计时数据写入每个文件的 lint 结果,供格式化器(formatter)展示,并不会打开面板。

在包含 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 启动 inspector。给 dev 命令传入 --stats(具体写法可通过 --help 查看),打开统计面板,找出占用大部分时间的规则和插件。“第一条类型感知规则”的注意事项在这个视图中同样适用。静态构建生成的是快照,因此你可以在修改配置前保存一份,修改后再生成一份进行对比。

哪些 ESLint 规则通常最慢?

在计时输出的前列,总是反复出现以下四类 ESLint 规则:

  • 类型感知规则。 recommendedTypeChecked 等预设会添加依赖类型检查器的规则。相关配置可参阅 使用 ESLint 检查 TypeScript。
  • eslint-plugin-prettier。 该插件只有对整个文件运行 Prettier 才能发现格式差异,因此每个被检查的文件都要先由 ESLint 解析一次,再由 Prettier 解析一次。ESLint 与 Prettier 一文解释了这两个工具如何分工。
  • @stylistic/indent。 它会为每个文件中的每个 token 计算缩进,在大型代码库中,这部分工作量会不断累积。
  • eslint-plugin-import 规则,尤其是 import/no-cycle。 这些规则会让插件独立于 ESLint 自行解析文件并追踪导入关系,因此开销随代码库规模增长。该插件能捕获哪些问题,请参阅 提升代码质量的 ESLint 插件。

如何逐一修复慢规则

每类慢规则都有各自的修复方法。以下大部分建议来自 typescript-eslint 性能指南:

罪魁祸首慢的原因修复方法
类型感知规则需要调用类型检查器用 files 将其限定在源码文件,并收窄 tsconfig 的 include
eslint-plugin-prettier对每个文件进行第二次解析移除该插件,改为运行 prettier --check .
@stylistic/indent需要处理每一个 token移除该规则,交由 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"] }

用 Prettier 的 --check 作为单独的 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',
  },
});

优化后的配置将类型感知规则限定在 src 中,移除了格式化相关插件以及 TypeScript 已覆盖的 import 规则,并且只在设置了某个环境变量时才启用 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在列表中消失
第一条类型感知规则显示为最慢仍排第一;请与 tsc 耗时对比

总结

在移除任何规则之前,先进行测量。首先获取 tsc 基准耗时,然后运行 TIMING=1 和 Config Inspector 统计面板,并逐一为可疑的类型感知规则计时。弄清楚哪些规则开销最大之后,本地和 pre-commit 阶段只保留快速的单文件规则,把跨文件检查和格式化交给 CI。现在就在你的项目上运行 TIMING=1 npx eslint .,并对照修复表处理排在最前面的规则。

常见问题

如何在 ESLint 的 TIMING 输出中查看超过 10 条规则?

将 TIMING 设置为更大的数字或 all。TIMING=1 显示耗时最长的十条规则,TIMING=50 显示五十条,TIMING=all 显示所有规则。每一行给出该规则的耗时(毫秒,包括规则创建和执行时间)及其在规则总耗时中的占比。如需为单条规则计时,可将 --no-config-lookup 与 --rule 结合使用。这种方式适用于无需解析器配置的核心规则。

ESLint 的 --cache 与类型感知规则一起使用是否安全?

并不完全安全。ESLint 的缓存只判断文件本身或配置是否发生变化,而忽略该文件所依赖的其他文件的变化。因此,类型感知的 typescript-eslint 规则以及 eslint-plugin-import 中需要解析导入的规则可能会给出过时的结果,而且两个方向都有可能:既可能漏报新问题,也可能持续报告早已修复的问题。typescript-eslint FAQ 不推荐使用 --cache,因此 CI 中应禁用它。

如何从 ESLint 获取按文件、按规则的耗时数据?

使用 ESLint 自身的 --stats 标志运行,并搭配能输出 stats 数据的格式化器,例如 npx eslint --stats -f json。每个结果对象都会带有一个 stats 键,其中包含该文件的解析耗时、修复耗时以及各规则的 lint 耗时;而 TIMING 只汇总整次运行。并非所有格式化器都会输出 stats 数据。关于第一条类型感知规则缓存的注意事项同样适用于 --stats。

parserOptions.projectService 是否比 parserOptions.project 让类型化 lint 更快?

不一定。typescript-eslint 原本预期从 v8.0.0 开始提供的 Project Service 会更快,并表示正将未能实现提速视为 bug 处理。它的主要好处是配置更简单:不再需要编写 parserOptions.project 的 glob,而其中递归的 ** 模式会带来额外的磁盘 IO。请尽量缩小 allowDefaultProject 的范围,因为它匹配的每个文件都会明显拖慢 lint 速度,并且默认情况下它最多只能匹配 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.