I previously wrote about profiling ESLint and removing slow running rules.
Problem: Slow Lint Run Process
While setting up our team's ESLint config with TypeScript, I noticed that running ESLint with typescript-eslint
was quite slow.
After digging around GitHub issues, I discovered the root cause was that running typescript-eslint
with type information incurs overheard performance cost.
typescript-eslint Converts the TypeScript AST into an ESLint AST
Docs: typescript-eslint: How does typescript-eslint work.
ESLint works by running rules against an AST that is generated by the ESLint JavaScript parser, Espree.
However, TypeScript has additional syntax, so the TypeScript AST that is created by the TypeScript compiler needs to be converted into an ESLint compatible AST.
This process is handled by a custom parser provided by @typescript-eslint/parser
which leverages ESTree in the @typescript-eslint/typescript-estree
package.
Thus, @typescript-eslint/typescript-estree
needs to invoke the TypeScript compiler on every .{ts,tsx}
file to produce the TypeScript AST, and then convert the TypeScript AST into an ESLint compatible AST.
Once this AST is created, the @typescript-eslint/eslint-plugin
extends rules with TypeScript-specific features that ESLint can run rules against.
The general .eslintrc
for typescript-eslint
is quite indicative of the processes that need to be run:
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
parserOptions: {
tsconfigRootDir: __dirname,
project: ["./tsconfig.json"]
},
plugins: ["@typescript-eslint"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
]
};
In summary the high level typescript-eslint parsing process involves:
@typescript-eslint/parser
@typescript-eslint/parser
reads.eslintrc
configs to determine included and excluded files@typescript-eslint/parser
invokes@typescript-eslint/typescript-estree
@typescript-eslint/typescript-estree
@typescript-eslint/typescript-estree
invokes the TypeScript compiler and parses every included.{ts,tsx}
files in to the TypeScript AST@typescript-eslint/typescript-estree
converts the TypeScript AST into an ESLint compatible AST
@typescript-eslint/eslint-plugin
@typescript-eslint/eslint-plugin
extends ESLint rules with TypeScript-specific features
TypeScript is basically performing a build of our project and copying and converting every AST node into an ESLint compatible node before ESLint can do its linting.
ESLint also runs each file in isolation, so there is duplicate overhead work of initializing the type checker in every single file.
Unfortunately, this means the runtime increases with the size of a project.
When there are more files, the TypeScript compile time and AST conversion time increases.
Next Steps: Minor Performance Improvement Tweaks
I wasn't able to discover many large optimizations to improve the runtime performance.
The underlying issue is still the overhead of the TypeScript compiler generating an AST, and the @typescript-eslint/typescript-estree
work of converting this AST to an ESLint compatible AST.
However, there are some minor tweaks:
- Set up
.eslintignore
to ignore irrelevant directories likenode_modules
and non-typescript files - Use the
--cache
flag when running eslint:eslint --cache \*\*/\_.ts
. Store the info about processed files in order to only operate on the changed ones.
There is also a whole article on TypeScript performance which recommends:
- Set up
tsconfig.json
with theinclude
property to specify only input folders in a project with TypeScript files that should be compiled. - Avoid adding too many
exclude
andinclude
folders, since TypeScript files must be discovered by walking through included directories, so running through many folders can actually slow down compilation.
module.exports = {
compilerOptions: {
// ...
},
include: ["src"],
exclude: ["**/node_modules", "**/.*/"]
};