perf(linter): speed up hot rules on large codebases#23829
Conversation
Optimize several rules that showed up in x-web --debug timings: - eslint/no-unused-vars: precompute exported local names once per file - oxc/no-this-in-exported-function: skip AST visit when body has no "this" - react/exhaustive-deps: classify hooks before ancestor scope walks - vitest/jest no-commented-out-tests: substring prefilter before regex - import/extensions: avoid String allocs for common lowercase extensions - unicorn/prefer-number-properties: name-filter identifiers before global lookup - eslint/no-useless-call: early reject non-call/apply members Benchmarked on ~/workspace/x-web with --threads=1 --debug timings.
Second pass on rules still prominent in large-repo timings: - prefer-number-properties: move bare identifiers to run_once via root_unresolved_references (drops IdentifierReference from NODE_TYPES) - no-commented-out-tests: replace regex with a hand-rolled multiline scanner - exhaustive-deps: gate on use*/React.use* before name/hook classification - import/extensions: no-op fast path for empty/default config - no-useless-call: fast path for plain StaticMemberExpression callees - no-unused-vars: skip has_usages walk for exported symbols Also regenerate rule_runner_impls for prefer-number-properties.
- no-restricted-globals / bad-array-method-on-arguments: walk only relevant unresolved references (run_once) instead of every identifier - no-obj-calls: skip binding resolution for non-matching direct globals - no-nonoctal-decimal-escape: byte scan for \8/\9 prefilter - interactive-supports-focus: cheap handler/role gates before element type - no-array-index-key: skip ancestor walk without key=; match method names - no-extend-native: ASCII uppercase gate before is_ecma_script_global - no-thenable: fast path for static `then` object keys - valid-expect / no-standalone-expect: avoid extra sort; reserve hash map
Use only `run_once` (not `run`+`run_once`) so lintgen keeps RUN_FUNCTIONS as RunOnce and does not visit every AST node. Handle window/globalThis member access from unresolved global-object references. Dedupe global_objects when scanning.
5a2d630 to
a9cd740
Compare
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
This optimization makes assumptions abount semantic that are incorrect. Hence this change is blocked by #23844
(no need to action this, I need to have a proper look next week)
- Split from #23829; original implementation by Yagiz Nizipli, retained as a commit coauthor. - Skip `ThisExpressionFinder` traversal when an exported function body does not contain the `this` substring. Original large-monorepo timings reported in #23829: 15.5 ms before, 2.1 ms after (~7.5x). Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
…as bytes (#23850) - Split from #23829; original implementation by Yagiz Nizipli, retained as a commit coauthor. - Scan string literal bytes for non-octal decimal escape sequences instead of decoding UTF-8 characters. - Use `array_windows` to keep the adjacent-byte scan clear and readable. Original large-monorepo timings reported in #23829: 6.5 ms before, 3.6 ms after (~1.8x). Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
…#23851) - Split from #23829; original implementation by Yagiz Nizipli, retained as a commit coauthor. - Skip lowercase identifiers before checking the ECMAScript global tables. - Preserve the existing Unicode-aware name filtering semantics while avoiding unnecessary global lookups. Original large-monorepo timings reported in #23829: 7.5 ms before, 5.7 ms after (~1.3x). Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
…ons first (#23854) - Split from #23829; original implementation by Yagiz Nizipli, retained as a commit coauthor. - Return early for JSX elements without interactive handlers or a static `role`. - Delay element-type and accessibility settings lookups until the cheaper checks pass. Original large-monorepo timings reported in #23829: 7.7 ms before, 2.4 ms after (~3.2x). Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
) Split from #23829. Delay `find_index_param_symbol_id` until after finding a relevant JSX `key` attribute or `React.cloneElement` key property. Use lazy, peekable iterators so the symbol lookup runs once only when needed while still checking every matching key. Add regression coverage for duplicate JSX attributes and object properties. **Stack** - #23857 👈 this PR! - #23856 - #23855 Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
Split from #23829. Adds a fast path when the comment definietly contains no test APIs Replaces the regex with a manual impl to improve perf Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
|
Thanks for putting this together! Theres a ton of useful perf work in here! I split the pieces that were straightforward to validate into smaller PRs so each rule change had its own review surface, tests, and performance/correctness tradeoff. In retrospect this also makes the history much easier to bisect or revert if one rule-specific optimization behaves differently on a real codebase. Pieces from this PR that have shipped separately:
I am splitting the remaining worthwhile pieces into separate follow-up PRs as well ( Again, thank you for the benchmarking and implementaiton work here! The smaller PRs are mostly just to make review and maintenance easier, not because the overall direction was wrong. Hope to see you around! |
## Summary - replace the small non-callable global list lookup with a direct `matches!` helper - defer `resolve_global_binding` scoping/node setup until after its existing global-reference check Credit to Yagiz Nizipli for the original work in #23829. Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
…ns (#24075) ## Summary - split out the remaining safe `import/extensions` performance slice from #23829 for easier review and bisectability - skip the rule work early when the config is effectively empty/default and there is nothing to enforce - parse written extensions as `Cow<str>` so common lowercase specifiers stay borrowed - preserve the existing written-extension behavior for cases like written `.js` resolving to `.ts` Credit to Yagiz Nizipli for the original work in #23829. Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
## Summary - Split the `eslint/no-useless-call` fast path out from #23829 for easier review and bisectability. - Add a direct static-member callee classifier for `foo.call(...)` / `foo.apply(...)`, so common non-`.call` / non-`.apply` static calls return before the generic member-expression fallback. - Keep the fallback for parenthesized, chained, computed, and optional-chain cases, with parenthesized regression coverage. Credit to Yagiz Nizipli for the original work in #23829. Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
## Summary - replace the small non-callable global list lookup with a direct `matches!` helper - defer `resolve_global_binding` scoping/node setup until after its existing global-reference check Credit to Yagiz Nizipli for the original work in #23829. Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
…ns (#24075) ## Summary - split out the remaining safe `import/extensions` performance slice from #23829 for easier review and bisectability - skip the rule work early when the config is effectively empty/default and there is nothing to enforce - parse written extensions as `Cow<str>` so common lowercase specifiers stay borrowed - preserve the existing written-extension behavior for cases like written `.js` resolving to `.ts` Credit to Yagiz Nizipli for the original work in #23829. Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
## Summary - Split the `eslint/no-useless-call` fast path out from #23829 for easier review and bisectability. - Add a direct static-member callee classifier for `foo.call(...)` / `foo.apply(...)`, so common non-`.call` / non-`.apply` static calls return before the generic member-expression fallback. - Keep the fallback for parenthesized, chained, computed, and optional-chain cases, with parenthesized regression coverage. Credit to Yagiz Nizipli for the original work in #23829. Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
- Split from #23829; original implementation by Yagiz Nizipli, retained as a commit coauthor. - Skip `ThisExpressionFinder` traversal when an exported function body does not contain the `this` substring. Original large-monorepo timings reported in #23829: 15.5 ms before, 2.1 ms after (~7.5x). Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
…as bytes (#23850) - Split from #23829; original implementation by Yagiz Nizipli, retained as a commit coauthor. - Scan string literal bytes for non-octal decimal escape sequences instead of decoding UTF-8 characters. - Use `array_windows` to keep the adjacent-byte scan clear and readable. Original large-monorepo timings reported in #23829: 6.5 ms before, 3.6 ms after (~1.8x). Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
…#23851) - Split from #23829; original implementation by Yagiz Nizipli, retained as a commit coauthor. - Skip lowercase identifiers before checking the ECMAScript global tables. - Preserve the existing Unicode-aware name filtering semantics while avoiding unnecessary global lookups. Original large-monorepo timings reported in #23829: 7.5 ms before, 5.7 ms after (~1.3x). Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
…ons first (#23854) - Split from #23829; original implementation by Yagiz Nizipli, retained as a commit coauthor. - Return early for JSX elements without interactive handlers or a static `role`. - Delay element-type and accessibility settings lookups until the cheaper checks pass. Original large-monorepo timings reported in #23829: 7.7 ms before, 2.4 ms after (~3.2x). Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
) Split from #23829. Delay `find_index_param_symbol_id` until after finding a relevant JSX `key` attribute or `React.cloneElement` key property. Use lazy, peekable iterators so the symbol lookup runs once only when needed while still checking every matching key. Add regression coverage for duplicate JSX attributes and object properties. **Stack** - #23857 👈 this PR! - #23856 - #23855 Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
Split from #23829. Adds a fast path when the comment definietly contains no test APIs Replaces the regex with a manual impl to improve perf Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
…#23880) Split from #23829. Move bare `NaN`/`Infinity` and method shorthand handling to `run_once` over root unresolved references, leaving call expressions and global-object member replacements in `run`. This reduces identifier-reference visits on large files while preserving behavior. Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
) Split from #23829. Precompute exported local names once per file so `eslint/no-unused-vars` can check exported bindings in O(1) instead of repeatedly scanning export entries. Skip usage walks for exported symbols and adapt the single-symbol import-fixer call site to preserve behavior. AI-assisted with Codex. Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
…3882) Split from #23829. Ports the exhaustive-deps hook-classification fast path from #23829 to avoid ancestor walks for non-reactive calls. Classify reactive hooks before the enclosing-function scope lookup so ordinary calls can return immediately while preserving behavior. Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
## Summary - replace the small non-callable global list lookup with a direct `matches!` helper - defer `resolve_global_binding` scoping/node setup until after its existing global-reference check Credit to Yagiz Nizipli for the original work in #23829. Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
…ns (#24075) ## Summary - split out the remaining safe `import/extensions` performance slice from #23829 for easier review and bisectability - skip the rule work early when the config is effectively empty/default and there is nothing to enforce - parse written extensions as `Cow<str>` so common lowercase specifiers stay borrowed - preserve the existing written-extension behavior for cases like written `.js` resolving to `.ts` Credit to Yagiz Nizipli for the original work in #23829. Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
## Summary - Split the `eslint/no-useless-call` fast path out from #23829 for easier review and bisectability. - Add a direct static-member callee classifier for `foo.call(...)` / `foo.apply(...)`, so common non-`.call` / non-`.apply` static calls return before the generic member-expression fallback. - Keep the fallback for parenthesized, chained, computed, and optional-chain cases, with parenthesized regression coverage. Credit to Yagiz Nizipli for the original work in #23829. Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
Summary
Speeds up lint rules that were prominent in
--debug timingson a large monorepo (x-web). Optimizations focus on cheaper gates, fewer AST dispatches (run_onceover unresolved references instead of every identifier), and avoiding work when rules are not applicable.Benchmark (release
oxlinton~/workspace/x-web, 5763 files, 260 rules,--silent --threads=1 --debug timings, three runs averaged; baseline = timings from before this PR):oxc/bad-array-method-on-argumentseslint/no-restricted-globalsoxc/no-this-in-exported-functionvitest/no-commented-out-testseslint/no-useless-callimport/extensionsjsx_a11y/interactive-supports-focusunicorn/prefer-number-propertiesunicorn/no-thenableeslint/no-nonoctal-decimal-escapereact/no-array-index-keyeslint/no-obj-callsreact/exhaustive-depseslint/no-extend-nativevitest/no-standalone-expectvitest/valid-expecteslint/no-unused-varshas_usages)Wall clock for the timed run was ~16.7s on 5763 files with 1 thread (machine-dependent). Per-rule ms can vary ±20% across runs; large call-count drops (
no-restricted-globals,bad-array-method-on-arguments,prefer-number-properties) are the durable wins.Changes (high level)
Identifier / unresolved-reference paths
eslint/no-restricted-globals,oxc/bad-array-method-on-arguments:run_onceover only relevantroot_unresolved_references(not everyIdentifierReference). Member access viawindow/globalThishandled from those refs whencheckGlobalObjectis set. Do not implement bothrunandrun_once— that forcesNODE_TYPES = Noneand visits every AST node.unicorn/prefer-number-properties: bareNaN/Infinity/ method shorthand viarun_once+ unresolved names;runonly for calls / global object members (dropsIdentifierReferencefromNODE_TYPES).eslint/no-unused-vars: precompute exported local names once per file for O(1) export checks; skiphas_usagesfor exported symbols.Call / JSX / comment hot paths
react/exhaustive-deps: gate onuse*/React.use*then reactive-hook classification before ancestor walks.eslint/no-useless-call: fast path for plainStaticMemberExpression.call/.apply.eslint/no-obj-calls: skip binding resolution for direct non-matching globals.jsx_a11y/interactive-supports-focus: require interactive handler +rolebefore element-type / a11y helpers.react/no-array-index-key: skip ancestor walk withoutkey=; method names viamatch.vitest/jestno-commented-out-tests: hand-rolled multiline scanner (no regex on every comment) with cheap keyword prefilter.import/extensions: no-op fast path for empty/default config; cheaper extension parsing (Cowfor written extensions).oxc/no-this-in-exported-function: skipThisExpressionFinderwhen body text has nothis.unicorn/no-thenable: fast path for staticthenkeys.eslint/no-extend-native: ASCII-uppercase gate beforeis_ecma_script_global.eslint/no-nonoctal-decimal-escape: byte scan for\8/\9prefilter.valid-expect/no-standalone-expect: drop extra sort; reserve node map (remaining cost is largely sharedcollect_possible_jest_call_node).Test plan
cargo test -p oxc_linter --lib(1162 passed)cargo lint/ CI Lint job (-D warnings) after clippysingle_match_elsefix onno-useless-callRUSTDOCFLAGS='-D warnings' cargo doc/ CI Doc joboxlint . --silent --threads=1 --debug timingson~/workspace/x-web(3 runs, table above)AI-assisted implementation; human-reviewed before submit.