Skip to main content

性能

使用类型信息进行代码检查 中所述,如果你使用类型感知的 linting,你的 lint 时间应该与你的构建时间大致相同。ESLint 规则中的大多数性能下降都来自类型感知 lint 规则调用 TypeScript 的类型检查 API。

¥As mentioned in Linting with Type Information, if you're using type-aware linting, your lint times should be roughly the same as your build times. Most performance slowdowns in ESLint rules are from type-aware lint rules calling to TypeScript's type checking APIs.

如果你遇到的 lint 时间比类型检查时间慢得多,那么有几个常见的罪魁祸首。

¥If you're experiencing lint times much slower than type-checking times, then there are a few common culprits.

缓慢的 ESLint 规则

¥Slow ESLint Rules

ESLint 包含 配置文件规则性能 中记录的 TIMING=1 选项,可从高级概述规则速度。但是,由于 TypeScript 使用内部缓存,因此项目的第一个类型感知 lint 规则几乎总是看起来最慢。

¥ESLint includes a TIMING=1 option documented in Profile Rule Performance that give a high-level overview of rule speeds. However, because TypeScript utilizes internal caching, a project's first type-aware lint rule will almost always seem the slowest.

在调查项目中哪些 lint 规则最慢时,请务必一次运行它们并分别比较这些时间测量值。

¥When investigating which lint rules are the slowest in your project, be sure to run them one at a time and compare those timing measurements separately.

要启用更完整的详细日志记录,你可以使用以下任何一种:

¥To enable more complete verbose logging, you can use any of:

  • eslint --debug:在 CLI 上启用所有 ESLint 的调试日志

    ¥eslint --debug: to enable all of ESLint's debug logs on the CLI

  • parserOptions.debugLevel:设置 eslinttypescript 和/或 typescript-eslint 的快捷方式

    ¥parserOptions.debugLevel: a shortcut to set eslint, typescript, and/or typescript-eslint

  • 直接为 debug 设置 DEBUG 环境变量:例如 DEBUG=typescript-eslint:* eslint

    ¥Directly setting the DEBUG environment variable for debug: e.g. DEBUG=typescript-eslint:* eslint

缓慢的 TypeScript 类型

¥Slow TypeScript Types

在项目上运行类型化 linting 通常与对同一项目进行类型检查一样慢。如果 TypeScript 的类型检查器在你的项目上运行缓慢,那么类型化的 linting 也会如此。

¥Running typed linting on a project is generally as slow as type checking that same project. If TypeScript's type checker runs slowly on your project, then typed linting will as well.

TypeScript Wiki 的性能页面 包括一般性能提示和调查慢速类型检查的步骤。特别是对于类型 linting:

¥The TypeScript Wiki's Performance page includes general performance tips and steps to investigate slow type checking. In particular for typed linting:

广泛包含在你的 tsconfig

¥Wide includes in your tsconfig

使用类型感知 linting 时,你需要向我们提供一个或多个 tsconfig。然后,我们将预解析所有文件,以便提供完整且完整的类型信息。

¥When using type-aware linting, you provide us with one or more tsconfigs. We then will pre-parse all files so that full and complete type information is available.

如果你在 include(例如 **/*)中提供非常宽的 glob,则可能会导致比你预期包含在此预解析中的文件多得多。此外,如果你在 tsconfig 中不提供 include,则它与提供最宽的 glob 相同。

¥If you provide very wide globs in your include (such as **/*), it can cause many more files than you expect to be included in this pre-parse. Additionally, if you provide no include in your tsconfig, then it is the same as providing the widest glob.

宽范围可能会导致 TypeScript 解析诸如构建工件之类的内容,这会严重影响性能。始终确保提供针对你特别想要检查的文件夹的 glob。

¥Wide globs can cause TypeScript to parse things like build artifacts, which can heavily impact performance. Always ensure you provide globs targeted at the folders you are specifically wanting to lint.

项目服务问题

¥Project Service Issues

projectServiceextraFileExtensions 的更改

¥Changes to extraFileExtensions with projectService

如果在同一项目中的文件之间使用不同的 extraFileExtensions 并使用 projectService 选项,则可能会导致性能下降。对于每个 linting 的文件,每当 extraFileExtensions 发生变化时,我们都会更新 projectService。这会导致底层 TypeScript 服务器执行完整的项目重新加载。

¥Using a different extraFileExtensions between files in the same project with the projectService option may cause performance degradations. For every file linted, we update the projectService whenever extraFileExtensions changes. This causes the underlying TypeScript server to perform a full project reload.

eslint.config.js
// @ts-check

import tseslint from 'typescript-eslint';
import vueParser from 'vue-eslint-parser';

const extraFileExtensions = ['.vue'];
export default [
{
files: ['*.ts'],
languageOptions: {
parser: tseslint.parser,
parserOptions: {
projectService: true,
extraFileExtensions,
},
},
},
{
files: ['*.vue'],
languageOptions: {
parser: vueParser,
parserOptions: {
projectService: true,
parser: tseslint.parser,
extraFileExtensions: ['.vue'],
extraFileExtensions,
},
},
},
];

可以使用 调试环境变量 观察项目重新加载:DEBUG='typescript-eslint:typescript-estree:*'

¥Project reloads can be observed using the debug environment variable: DEBUG='typescript-eslint:typescript-estree:*'.

typescript-estree:useProgramFromProjectService Updating extra file extensions: before=[]: after=[ '.vue' ]
typescript-estree:tsserver:info reload projects.
typescript-estree:useProgramFromProjectService Extra file extensions updated: [ '.vue' ]
...
typescript-estree:useProgramFromProjectService Updating extra file extensions: before=[ '.vue' ]: after=[]
typescript-estree:tsserver:info reload projects.
typescript-estree:useProgramFromProjectService Extra file extensions updated: []
...
typescript-estree:tsserver:info Scheduled: /path/to/tsconfig.src.json, Cancelled earlier one +0ms
typescript-estree:tsserver:info Scheduled: *ensureProjectForOpenFiles*, Cancelled earlier one +0ms
...
typescript-estree:useProgramFromProjectService Updating extra file extensions: before=[]: after=[ '.vue' ]
typescript-estree:tsserver:info reload projects.
typescript-estree:useProgramFromProjectService Extra file extensions updated: [ '.vue' ]

传统项目问题

¥Traditional Project issues

广泛包含在你的 ESLint 选项中

¥Wide includes in your ESLint options

提示

v8 中的新 "项目服务" 不需要对宽 TSConfig 包含进行额外配置。如果你使用 parserOptions.projectService,则此问题已为你解决。

¥The new "project service" in v8 requires no additional configuration for wide TSConfig includes. If you're using parserOptions.projectService, this problem is solved for you.

在 ESLint parserOptions.project 配置中指定 tsconfig.json 路径也可能会导致比预期更多的磁盘 IO。不要使用 ** 递归检查所有文件夹的 glob,而要选择一次使用单个 * 的路径。

¥Specifying tsconfig.json paths in an ESLint parserOptions.project configuration is also likely to cause much more disk IO than expected. Instead of globs that use ** to recursively check all folders, prefer paths that use a single * at a time.

eslint.config.mjs
// @ts-check

import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';

export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.recommendedRequiringTypeChecking,
{
languageOptions: {
parserOptions: {
tsconfigRootDir: import.meta.dirname,
project: ['./**/tsconfig.json'],
project: ['./packages/*/tsconfig.json'],
},
},
},
);

请参阅 解析器选项 "project" 中的 Glob 模式会减慢 linting 以了解更多详细信息。

¥See Glob pattern in parser's option "project" slows down linting for more details.

第三方插件

¥Third-Party Plugins

@stylistic/ts/indent 和其他风格规则

¥@stylistic/ts/indent and other stylistic rules rules

@stylisic/ts/indent 规则 有助于确保你的代码库遵循一致的缩进模式。然而,这涉及文件中每个标记的大量计算。在大型代码库中,这些问题可能会累积起来,并严重影响性能。

¥The @stylisic/ts/indent rule helps ensure your codebase follows a consistent indentation pattern. However this involves a lot of computations across every single token in a file. Across a large codebase, these can add up, and severely impact performance.

我们建议不要使用此规则,而是使用 prettier 之类的工具来强制执行标准化格式。

¥We recommend not using this rule, and instead using a tool like prettier to enforce a standardized formatting.

有关更多信息,请参阅我们的 格式化文档

¥See our documentation on formatting for more information.

eslint-plugin-prettier

该插件在 lint 时显示 Prettier 格式化问题,有助于确保你的代码始终格式化。但是,这需要付出相当大的代价 - 为了弄清是否存在差异,它必须对每个要进行 lint 的文件执行 Prettier 格式化。这意味着每个文件将被解析两次 - ESLint 一次,Prettier 一次。这可能会增加大型代码库的规模。

¥This plugin surfaces Prettier formatting problems at lint time, helping to ensure your code is always formatted. However this comes at a quite a large cost - in order to figure out if there is a difference, it has to do a Prettier format on every file being linted. This means that each file will be parsed twice - once by ESLint, and once by Prettier. This can add up for large codebases.

我们建议不要使用此插件,而是使用 Prettier 的 --check 标志来检测文件是否格式正确。例如,我们的 CI 设置为自动运行以下命令,这会阻止尚未格式化的 PR:

¥Instead of using this plugin, we recommend using Prettier's --check flag to detect if a file has not been correctly formatted. For example, our CI is setup to run the following command automatically, which blocks PRs that have not been formatted:

npm run prettier --check .

请参阅 Prettier 的 --check 文档 以了解更多详细信息。

¥See Prettier's --check docs for more details.

eslint-plugin-import

这是我们在这个项目中使用的另一个很棒的插件。然而,有一些规则可能会导致 lint 非常慢,因为它们会导致插件进行自己的解析和文件跟踪。这种双重解析加起来会产生很大的代码库。

¥This is another great plugin that we use ourselves in this project. However there are a few rules which can cause your lints to be really slow, because they cause the plugin to do its own parsing, and file tracking. This double parsing adds up for large codebases.

进行单文件静态分析的规则有很多,但我们提供以下建议。

¥There are many rules that do single file static analysis, but we provide the following recommendations.

我们建议你不要使用以下规则,因为 TypeScript 提供与标准类型检查相同的检查:

¥We recommend you do not use the following rules, as TypeScript provides the same checks as part of standard type checking:

  • import/named

  • import/namespace

  • import/default

  • import/no-named-as-default-member

  • import/no-unresolved(只要你使用 import over require

    ¥import/no-unresolved (as long as you are using import over require)

以下规则在 TypeScript 中没有等效检查,因此我们建议你仅在 CI/推送时运行它们,以减轻本地性能负担。

¥The following rules do not have equivalent checks in TypeScript, so we recommend that you only run them at CI/push time, to lessen the local performance burden.

  • import/no-named-as-default

  • import/no-cycle

  • import/no-unused-modules

  • import/no-deprecated

使用 import/extensions 强制扩展

¥import/extensions enforcing extensions are used

如果你想强制始终使用文件扩展名,并且你没有使用 moduleResolution node16nodenext,那么对你来说真的没有好的选择,你应该继续使用 import/extensions lint 规则。

¥If you want to enforce file extensions are always used and you're NOT using moduleResolution node16 or nodenext, then there's not really a good alternative for you, and you should continue using the import/extensions lint rule.

如果你想强制始终使用文件扩展名,并且你正在使用 moduleResolution node16nodenext,那么你根本不需要使用 lint 规则,因为 TypeScript 会自动强制你包含扩展名!

¥If you want to enforce file extensions are always used and you ARE using moduleResolution node16 or nodenext, then you don't need to use the lint rule at all because TypeScript will automatically enforce that you include extensions!

未使用 import/extensions 强制扩展

¥import/extensions enforcing extensions are not used

从表面上看,import/extensions 似乎应该适用于这种用例,但该规则不仅仅是一个纯粹的 AST 检查 - 它必须解析磁盘上的模块,以便在你导入带有扩展名作为其名称一部分的模块的情况下不会出现误报(例如,foo.js 解析为 node_modules/foo.js/index.js,因此需要 .js)。这种磁盘查找的成本很高,因此会使规则变慢。

¥On the surface import/extensions seems like it should be fast for this use case, however the rule isn't just a pure AST-check - it has to resolve modules on disk so that it doesn't false positive on cases where you are importing modules with an extension as part of their name (eg foo.js resolves to node_modules/foo.js/index.js, so the .js is required). This disk lookup is costly and thus makes the rule slow.

如果你的项目不使用任何名称中带有文件扩展名的 npm 包,也没有使用两个扩展名(如 bar.js.ts)来命名文件,那么这笔额外的费用可能不值得,你可以使用 no-restricted-syntax lint 规则进行更简单的检查。

¥If your project doesn't use any npm packages with a file extension in their name, nor do you name your files with two extensions (like bar.js.ts), then this extra cost probably isn't worth it, and you can use a much simpler check using the no-restricted-syntax lint rule.

下面的配置比 import/extensions 快几个数量级,因为它不进行磁盘查找,但是对于前面提到的 foo.js 模块等情况,它会误报。

¥The below config is several orders of magnitude faster than import/extensions as it does not do disk lookups, however it will false-positive on cases like the aforementioned foo.js module.

function banImportExtension(extension) {
const message = `Unexpected use of file extension (.${extension}) in import`;
const literalAttributeMatcher = `Literal[value=/\\.${extension}$/]`;
return [
{
// import foo from 'bar.js';
selector: `ImportDeclaration > ${literalAttributeMatcher}.source`,
message,
},
{
// const foo = import('bar.js');
selector: `ImportExpression > ${literalAttributeMatcher}.source`,
message,
},
{
// type Foo = typeof import('bar.js');
selector: `TSImportType > TSLiteralType > ${literalAttributeMatcher}`,
message,
},
{
// const foo = require('foo.js');
selector: `CallExpression[callee.name = "require"] > ${literalAttributeMatcher}.arguments`,
message,
},
];
}

module.exports = {
// ... other config ...
rules: {
'no-restricted-syntax': [
'error',
...banImportExtension('js'),
...banImportExtension('jsx'),
...banImportExtension('ts'),
...banImportExtension('tsx'),
],
},
};