Why typescript-eslint Performance is Slow

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:

.eslintrc.js
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 like node_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 the include property to specify only input folders in a project with TypeScript files that should be compiled.
  • Avoid adding too many exclude and include folders, since TypeScript files must be discovered by walking through included directories, so running through many folders can actually slow down compilation.
tsconfig.json
module.exports = {
  compilerOptions: {
    // ...
  },
  include: ["src"],
  exclude: ["**/node_modules", "**/.*/"]
};