Skip to content

chore(eslint-plugin): defer type checks to improve rules performance#12296

Merged
JoshuaKGoldberg merged 1 commit into
typescript-eslint:mainfrom
StyleShit:chore/11204-rules-perf
May 27, 2026
Merged

chore(eslint-plugin): defer type checks to improve rules performance#12296
JoshuaKGoldberg merged 1 commit into
typescript-eslint:mainfrom
StyleShit:chore/11204-rules-perf

Conversation

@StyleShit

@StyleShit StyleShit commented May 1, 2026

Copy link
Copy Markdown
Member

I was working on #11204, and realized that some rules might benefit from deferring the type checks after the AST structure checks.

I ran some benchmarks locally (M4 Pro, 24GB) against packages/eslint-plugin:

prefer-nullish-coalescing is about ~7% faster
$ hyperfine "eslint-before-fix" "eslint-after-fix"

Benchmark 1: eslint-before-fix
  Time (mean ± σ):      7.472 s ±  0.132 s    [User: 8.215 s, System: 0.979 s]
  Range (min … max):    7.288 s …  7.703 s    10 runs

Benchmark 2: eslint-after-fix
  Time (mean ± σ):      6.961 s ±  0.145 s    [User: 7.613 s, System: 0.964 s]
  Range (min … max):    6.845 s …  7.290 s    10 runs

Summary
  eslint-after-fix ran
    1.07 ± 0.03 times faster than eslint-before-fix
no-base-to-string is about ~4% faster
$ hyperfine "eslint-before-fix" "eslint-after-fix"

Benchmark 1: eslint-before-fix
  Time (mean ± σ):      6.189 s ±  0.163 s    [User: 6.511 s, System: 0.807 s]
  Range (min … max):    5.981 s …  6.473 s    10 runs

Benchmark 2: eslint-after-fix
  Time (mean ± σ):      5.930 s ±  0.207 s    [User: 6.294 s, System: 0.734 s]
  Range (min … max):    5.730 s …  6.372 s    10 runs

Summary
  eslint-after-fix ran
    1.04 ± 0.04 times faster than eslint-before-fix
no-confusing-void-expression is about ~32% faster
$ hyperfine "eslint-before-fix" "eslint-after-fix"

Benchmark 1: eslint-before-fix
  Time (mean ± σ):     11.270 s ±  0.129 s    [User: 13.346 s, System: 1.549 s]
  Range (min … max):   11.064 s … 11.468 s    10 runs

Benchmark 2: eslint-after-fix
  Time (mean ± σ):      8.534 s ±  0.122 s    [User: 10.208 s, System: 1.316 s]
  Range (min … max):    8.344 s …  8.742 s    10 runs

Summary
  eslint-after-fix ran
    1.32 ± 0.02 times faster than eslint-before-fix
no-duplicate-type-constituents is about ~2% faster
$ hyperfine "eslint-before-fix" "eslint-after-fix"

Benchmark 1: eslint-before-fix
  Time (mean ± σ):      5.369 s ±  0.166 s    [User: 4.750 s, System: 0.703 s]
  Range (min … max):    5.211 s …  5.757 s    10 runs

Benchmark 2: eslint-after-fix
  Time (mean ± σ):      5.249 s ±  0.058 s    [User: 4.657 s, System: 0.663 s]
  Range (min … max):    5.190 s …  5.343 s    10 runs

Summary
  eslint-after-fix ran
    1.02 ± 0.03 times faster than eslint-before-fix
no-unsafe-return is about ~3% faster
$ hyperfine "eslint-before-fix" "eslint-after-fix"

Benchmark 1: eslint-before-fix
  Time (mean ± σ):      8.811 s ±  0.113 s    [User: 10.341 s, System: 1.229 s]
  Range (min … max):    8.671 s …  9.040 s    10 runs

Benchmark 2: eslint-after-fix
  Time (mean ± σ):      8.566 s ±  0.104 s    [User: 10.145 s, System: 1.144 s]
  Range (min … max):    8.460 s …  8.823 s    10 runs

Summary
  eslint-after-fix ran
    1.03 ± 0.02 times faster than eslint-before-fix
strict-void-return is about ~2% faster
$ hyperfine "eslint-before-fix" "eslint-after-fix"

Benchmark 1: eslint-before-fix
  Time (mean ± σ):     12.029 s ±  0.236 s    [User: 14.407 s, System: 1.470 s]
  Range (min … max):   11.789 s … 12.471 s    10 runs

Benchmark 2: eslint-after-fix
  Time (mean ± σ):     11.830 s ±  0.117 s    [User: 14.200 s, System: 1.518 s]
  Range (min … max):   11.650 s … 12.016 s    10 runs

Summary
  eslint-after-fix ran
    1.02 ± 0.02 times faster than eslint-before-fix
Running all rules is about ~1% faster (probably negligible, which is why it doesn't really help with #11204)
$ hyperfine "eslint-before-fix" "eslint-after-fix"

Benchmark 1: eslint-before-fix
  Time (mean ± σ):     25.464 s ±  0.492 s    [User: 30.937 s, System: 3.279 s]
  Range (min … max):   24.840 s … 26.408 s    10 runs

Benchmark 2: eslint-after-fix
  Time (mean ± σ):     25.179 s ±  0.382 s    [User: 30.511 s, System: 3.244 s]
  Range (min … max):   24.710 s … 25.806 s    10 runs

Summary
  eslint-after-fix ran
    1.01 ± 0.02 times faster than eslint-before-fix

Some rules (i.e., require-array-sort-compare, prefer-regexp-exec) didn't show any performance gains from this change.

I guess they will in codebases that actually violate them. Currently, they're basically skipping the type checks anyway in our codebase, thanks to selectors like CallExpression[arguments.length=1] > MemberExpression together with a member name check (.match, .sort, etc.).
(Violating codebases might also show more performance gains for the rules in this PR, actually)

I'll open PRs for these rules if we go forward with this PR and decide that it's worth it.


Thanks to AI it was much faster to find areas where it was applicable

@typescript-eslint

Copy link
Copy Markdown
Contributor

Thanks for the PR, @StyleShit!

typescript-eslint is a 100% community driven project, and we are incredibly grateful that you are contributing to that community.

The core maintainers work on this in their personal time, so please understand that it may not be possible for them to review your work immediately.

Thanks again!


🙏 Please, if you or your company is finding typescript-eslint valuable, help us sustain the project by sponsoring it transparently on https://opencollective.com/typescript-eslint.

@netlify

netlify Bot commented May 1, 2026

Copy link
Copy Markdown

Deploy Preview for typescript-eslint ready!

Name Link
🔨 Latest commit c917f08
🔍 Latest deploy log https://app.netlify.com/projects/typescript-eslint/deploys/69f4b84b5769af00084747e9
😎 Deploy Preview https://deploy-preview-12296--typescript-eslint.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 99 (no change from production)
Accessibility: 97 (no change from production)
Best Practices: 100 (no change from production)
SEO: 90 (no change from production)
PWA: 80 (no change from production)
View the detailed breakdown and full score reports
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@nx-cloud

nx-cloud Bot commented May 1, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit c917f08

Command Status Duration Result
nx run-many -t lint ✅ Succeeded 2m 15s View ↗
nx run-many -t typecheck ✅ Succeeded 38s View ↗
nx test eslint-plugin-internal --coverage=false ✅ Succeeded 4s View ↗
nx run integration-tests:test ✅ Succeeded 5s View ↗
nx test typescript-estree --coverage=false ✅ Succeeded 1s View ↗
nx run types:build ✅ Succeeded 1s View ↗
nx run generate-configs ✅ Succeeded 6s View ↗
nx run-many --target=build --parallel --exclude... ✅ Succeeded 14s View ↗
Additional runs (34) ✅ Succeeded ... View ↗

☁️ Nx Cloud last updated this comment at 2026-05-01 14:34:04 UTC

@StyleShit StyleShit changed the title chore(eslint-plugin): improve rules performance chore(eslint-plugin): defer type checks to improve rules performance May 1, 2026
@codecov

codecov Bot commented May 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.47619% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.83%. Comparing base (5081014) to head (c917f08).

Files with missing lines Patch % Lines
...ckages/eslint-plugin/src/rules/no-unsafe-return.ts 33.33% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #12296   +/-   ##
=======================================
  Coverage   86.82%   86.83%           
=======================================
  Files         513      513           
  Lines       16490    16500   +10     
  Branches     5141     5144    +3     
=======================================
+ Hits        14317    14327   +10     
  Misses       1478     1478           
  Partials      695      695           
Flag Coverage Δ
unittest 86.83% <90.47%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...kages/eslint-plugin/src/rules/no-base-to-string.ts 99.24% <100.00%> (+<0.01%) ⬆️
...t-plugin/src/rules/no-confusing-void-expression.ts 100.00% <100.00%> (ø)
...plugin/src/rules/no-duplicate-type-constituents.ts 100.00% <100.00%> (ø)
...lint-plugin/src/rules/prefer-nullish-coalescing.ts 95.31% <100.00%> (ø)
...ages/eslint-plugin/src/rules/strict-void-return.ts 96.58% <100.00%> (+0.05%) ⬆️
...ckages/eslint-plugin/src/rules/no-unsafe-return.ts 94.11% <33.33%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@JoshuaKGoldberg JoshuaKGoldberg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yesss this is great!!

I've long wondered if we could enforce this kind of preference with a lint rule. I don't think it's practically doable, so maybe an agent skill would be a better fit?

@JoshuaKGoldberg JoshuaKGoldberg merged commit 2df540c into typescript-eslint:main May 27, 2026
73 of 75 checks passed
@StyleShit

Copy link
Copy Markdown
Member Author

An agent skill could be nice and useful. Should I raise a new issue for this?

@JoshuaKGoldberg

Copy link
Copy Markdown
Member

Yeah I think we'd want to think more holistically on that. Maybe we could provide agent rules as a part of the project!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants