Performance
As mentioned inLinting 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.
If you're experiencing lint times much slower than type-checking times, then there are a few common culprits.
Slow ESLint Rules
ESLint includes aTIMING=1 option documented inProfile Rule Performance that give a high-level overview of rule speeds.However, because TypeScript utilizes internal caching, a project'sfirst type-aware lint rule will almost always seem the slowest.
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: to enable all of ESLint's debug logs on the CLIparserOptions.debugLevel: a shortcut to seteslint,typescript, and/ortypescript-eslint- Directly setting the
DEBUGenvironment variable fordebug: e.g.DEBUG=typescript-eslint:* eslint
Slow TypeScript Types
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.
TheTypeScript Wiki's Performance page includes general performance tips and steps to investigate slow type checking.In particular for typed linting:
- Investigating Issues can spot slow types and type checking:
- Running
tscalone should provide a baseline for your full project's type checking speed. - Performance Tracing can spotlight specific slow types within your project.
- Running
- Using Project References -which requires enabling thenew "project service" (
parserOptions.projectService) in v8- can be helpful to speed up type checking on larger projects.
If none of the above work, you can try adjusting the--max-semi-space-size of Node. Increasing the max size of a semi-space can improve performance at the cost of more memory consumption. You canread more about setting space size in Node.js here.
You can enable the setting by prepending your ESLint command like:
NODE_OPTIONS=--max-semi-space-size=256 eslint<rest of your command>
Wide includes in yourtsconfig
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.
If you provide very wide globs in yourinclude (such as**/*), it can cause many more files than you expect to be included in this pre-parse.Additionally, if you provide noinclude in your tsconfig, then it is the same as providing the widest 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
Changes toextraFileExtensions withprojectService
Using a differentextraFileExtensions between files in the same project withtheprojectService option may cause performance degradations.For every file linted, we update theprojectService wheneverextraFileExtensions changes.This causes the underlying TypeScript server to perform a full project reload.
- Flat Config
- Legacy Config
// @ts-check
importtseslintfrom'typescript-eslint';
importvueParserfrom'vue-eslint-parser';
const extraFileExtensions=['.vue'];
exportdefault[
{
files:['*.ts'],
languageOptions:{
parser: tseslint.parser,
parserOptions:{
projectService:true,
extraFileExtensions,
},
},
},
{
files:['*.vue'],
languageOptions:{
parser: vueParser,
parserOptions:{
projectService:true,
parser: tseslint.parser,
extraFileExtensions:['.vue'],
extraFileExtensions,
},
},
},
];
const extraFileExtensions=['.vue'];
module.exports={
files:['*.ts'],
parser:'@typescript-eslint/parser',
parserOptions:{
projectService:true,
extraFileExtensions,
},
overrides:[
{
files:['*.vue'],
parser:'vue-eslint-parser',
parserOptions:{
parser:'@typescript-eslint/parser',
projectService:true,
extraFileExtensions:['.vue'],
extraFileExtensions,
},
},
],
};
Project reloads can be observed using thedebug 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
Wide includes in your ESLint options
Thenew "project service" in v8 requires no additional configuration for wide TSConfig includes.If you're usingparserOptions.projectService, this problem is solved for you.
Specifyingtsconfig.json paths in an ESLintparserOptions.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.
- Flat Config
- Legacy Config
// @ts-check
importeslintfrom'@eslint/js';
importtseslintfrom'typescript-eslint';
exportdefaultdefineConfig(
eslint.configs.recommended,
tseslint.configs.recommendedRequiringTypeChecking,
{
languageOptions:{
parserOptions:{
project:['./**/tsconfig.json'],
project:['./packages/*/tsconfig.json'],
},
},
},
);
module.exports={
extends:[
'eslint:recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
],
parser:'@typescript-eslint/parser',
parserOptions:{
tsconfigRootDir: __dirname,
project:['./**/tsconfig.json'],
project:['./packages/*/tsconfig.json'],
},
plugins:['@typescript-eslint'],
root:true,
};
SeeGlob pattern in parser's option "project" slows down linting for more details.
Third-Party Plugins
@stylistic/ts/indent and other stylistic rules
The@stylisic/ts/indent rule helps ensure your codebase follows a consistent indentation pattern.However this involves alot of computations across every single token in a file.Across a large codebase, these can add up, and severely impact performance.
We recommend not using this rule, and instead using a tool likeprettier to enforce a standardized formatting.
See ourdocumentation on formatting for more information.
eslint-plugin-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.
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
- Yarn
- pnpm
npm run prettier--check.
yarn prettier--check.
pnpm run prettier--check.
SeePrettier's--check docs for more details.
eslint-plugin-import
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.
We recommend you do not use the following rules, as TypeScript provides the same checks as part of standard type checking:
import/namedimport/namespaceimport/defaultimport/no-named-as-default-memberimport/no-unresolved(as long as you are usingimportoverrequire)
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-defaultimport/no-cycleimport/no-unused-modulesimport/no-deprecated
import/extensions enforcing extensions are used
If you want to enforce file extensions are always used and you'reNOT usingmoduleResolutionnode16 ornodenext, then there's not really a good alternative for you, and you should continue using theimport/extensions lint rule.
If you want to enforce file extensions are always used and youARE usingmoduleResolutionnode16 ornodenext, then you don't need to use the lint rule at all because TypeScript will automatically enforce that you include extensions!
import/extensions enforcing extensions are not used
On the surfaceimport/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 (egfoo.js resolves tonode_modules/foo.js/index.js, so the.js is required). This disk lookup is costly and thus makes the rule slow.
If your project doesn't use anynpm packages with a file extension in their name, nor do you name your files with two extensions (likebar.js.ts), then this extra cost probably isn't worth it, and you can use a much simpler check using theno-restricted-syntax lint rule.
The below config is several orders of magnitude faster thanimport/extensions as it does not do disk lookups, however it will false-positive on cases like the aforementionedfoo.js module.
functionbanImportExtension(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'),
],
},
};