Skip to content

perf(linter): dispatch react_perf rules only on JSXAttribute nodes#24084

Closed
anonrig wants to merge 2 commits into
oxc-project:mainfrom
anonrig:perf/linter-react-perf-jsx-attribute-dispatch
Closed

perf(linter): dispatch react_perf rules only on JSXAttribute nodes#24084
anonrig wants to merge 2 commits into
oxc-project:mainfrom
anonrig:perf/linter-react-perf-jsx-attribute-dispatch

Conversation

@anonrig

@anonrig anonrig commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What this PR does

The four react_perf rules (jsx-no-new-object-as-prop, jsx-no-new-array-as-prop, jsx-no-new-function-as-prop, jsx-no-jsx-as-prop) received their Rule implementation from a blanket impl<R> Rule for R where R: ReactPerfRule in utils/react_perf.rs. Because the run body lived outside the rule files, oxc_linter_codegen could not detect the node types they match and generated NODE_TYPES: None — so all four rules ran on every AST node of every JSX/TSX file, even though they only ever act on AstKind::JSXAttribute.

This PR replaces the blanket impl with per-rule Rule impls whose run starts with the let AstKind::JSXAttribute(..) = node.kind() else { return; } pattern that codegen detects, delegating the shared logic to a run_react_perf_rule helper. The generated runners now carry NODE_TYPES: [JSXAttribute], and with vue/max_props being the only remaining NODE_TYPES: None rule (gated to .vue files), the "run on any node" bucket is empty for typical configurations.

On kitchen-sink.tsx this removes ~133k wasted run invocations per rule per lint (2.66M over 20 iterations; the rules dropped from ~17% of instrumented rule time to ~0.4%).

A second commit adds an early exit to jest/no-confusing-set-timeout: every diagnostic it can report requires at least one jest.setTimeout reference, so the rule now collects those references first and skips building the possible-jest-node map and a second full pass over all references when there are none (the common case outside test setup files).

Benchmarks (local, Apple Silicon)

Retired instruction counts for Linter::run only (deterministic, measured via proc_pid_rusage, ConfigStoreBuilder::all() like the criterion bench):

bench file main this PR Δ
kitchen-sink.tsx 350.4M 324.6M −7.4%
App.tsx 224.1M 215.0M −4.1%
binder.ts 60.1M 59.3M −1.4%
RadixUIAdoptionSection.jsx 10.21M 10.10M −1.1%
react.development.js 29.7M 29.6M −0.4%

Wall-time (criterion, idle machine, back-to-back runs): kitchen-sink −4.9%, App.tsx −3.0%. CodSpeed should give the authoritative numbers.

Also evaluated and rejected: a chunked rule-major dispatch reorder (running each rule over batches of its matching nodes to make the RuleEnum dispatch branch predictable) — it measured 3–5% slower due to lost data-cache locality on node data, so it is not included.

Testing

  • All oxc_linter tests pass; in debug mode the Tester also asserts the optimized (type-bucketed) and unoptimized paths produce identical diagnostics for these rules.
  • cargo lintgen regenerated rule_runner_impls.rs (included); rerunning it is a no-op.
  • oxlint CLI tests pass (except lint::suppression::test_fixing_only_ts_go_errors, which also fails on main in my environment — missing tsgolint binary).

Disclosure

This PR was developed with the assistance of an AI coding agent (Grok), per the repo's AI usage policy. I have reviewed, tested, and take responsibility for the changes.

anonrig added 2 commits July 2, 2026 13:11
The four react_perf rules got their `Rule` implementation from a blanket
`impl<R> Rule for R where R: ReactPerfRule` in `utils/react_perf.rs`.
Because the `run` body lived outside the rule files, `oxc_linter_codegen`
could not detect the node types these rules match, generating
`NODE_TYPES: None` — so all four rules ran on every AST node of every
JSX/TSX file.

Replace the blanket impl with per-rule `Rule` impls whose `run` starts
with the `let AstKind::JSXAttribute(..) = node.kind() else` pattern that
codegen detects, delegating to a shared `run_react_perf_rule` helper.
The rules are now dispatched only to `JSXAttribute` nodes.
…t.setTimeout references

All three diagnostics this rule reports require at least one
`jest.setTimeout` member access. Collect those references first and skip
collecting possible jest call nodes and the second full pass over all
references when there are none, which is the common case for non-test
files.
@anonrig
anonrig requested a review from camc314 as a code owner July 2, 2026 17:12
@codspeed-hq

codspeed-hq Bot commented Jul 2, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 5.6%

⚡ 2 improved benchmarks
✅ 3 untouched benchmarks
⏩ 66 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation linter[kitchen-sink.tsx] 173 ms 161.5 ms +7.14%
Simulation linter[App.tsx] 105.6 ms 101.5 ms +4.08%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing anonrig:perf/linter-react-perf-jsx-attribute-dispatch (d1e3f24) with main (002ab35)2

Open in CodSpeed

Footnotes

  1. 66 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (cb2822d) during the generation of this report, so 002ab35 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@camc314 camc314 added the A-linter Area - Linter label Jul 2, 2026
@camc314

camc314 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Thanks for working on this! Looks like camchenry beat you to it! I'll split the jest change as that's still useful

@camc314 camc314 closed this Jul 2, 2026
graphite-app Bot pushed a commit that referenced this pull request Jul 2, 2026
)

### Summary

Extracts the Jest-only performance change from #24084.

Skips collecting possible Jest call nodes and the second full reference pass in `jest/no-confusing-set-timeout` when the file has no `jest.setTimeout` references. Every diagnostic emitted by this rule requires at least one `jest.setTimeout` reference, so this keeps behavior unchanged while avoiding work in the common case.

### Review

Reviewed #24084 and intentionally left out the separate `react_perf` dispatch changes so this PR contains only the Jest rule update.

Co-authored-by: Yagiz Nizipli <[email protected]>
camc314 added a commit that referenced this pull request Jul 3, 2026
)

### Summary

Extracts the Jest-only performance change from #24084.

Skips collecting possible Jest call nodes and the second full reference pass in `jest/no-confusing-set-timeout` when the file has no `jest.setTimeout` references. Every diagnostic emitted by this rule requires at least one `jest.setTimeout` reference, so this keeps behavior unchanged while avoiding work in the common case.

### Review

Reviewed #24084 and intentionally left out the separate `react_perf` dispatch changes so this PR contains only the Jest rule update.

Co-authored-by: Yagiz Nizipli <[email protected]>
camc314 added a commit that referenced this pull request Jul 3, 2026
)

### Summary

Extracts the Jest-only performance change from #24084.

Skips collecting possible Jest call nodes and the second full reference pass in `jest/no-confusing-set-timeout` when the file has no `jest.setTimeout` references. Every diagnostic emitted by this rule requires at least one `jest.setTimeout` reference, so this keeps behavior unchanged while avoiding work in the common case.

### Review

Reviewed #24084 and intentionally left out the separate `react_perf` dispatch changes so this PR contains only the Jest rule update.

Co-authored-by: Yagiz Nizipli <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-linter Area - Linter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants