Skip to content

perf: optimize new-process-route-tree.ts #7449

Open
schiller-manuel wants to merge 4 commits into
mainfrom
perf-process-route-tree
Open

perf: optimize new-process-route-tree.ts #7449
schiller-manuel wants to merge 4 commits into
mainfrom
perf-process-route-tree

Conversation

@schiller-manuel
Copy link
Copy Markdown
Collaborator

@schiller-manuel schiller-manuel commented May 20, 2026

Summary

  • Optimized route-tree post-processing by sorting only dynamic child arrays that can need ordering.
  • Applied the same sortable-array tracking to route masks.
  • Added a param decode fast path that skips decodeURIComponent when a matched value has no % escape.
  • Removed the recursive sortTreeNodes post-build tree walk.
  • Pass sortables through recursive route parsing so nested dynamic arrays are recorded during construction.
  • Skip sortable tracking for single-route lazy matching, where no sibling ordering can be needed.

Bundle Size

Scenario: react-router.minimal

Metric Before After Delta
gzip 89,392 89,269 -123
initial gzip 89,251 89,130 -121
raw 280,592 279,600 -992
brotli 77,753 77,670 -83

Focused Benchmarks

Benchmarks were compared against a baseline worktree with the same benchmark cases applied and only the implementation different.

Case Baseline hz Current hz Delta
processRouteTree static-heavy singleton dynamics 2,953.62 3,315.89 +12.26%
processRouteTree sortable dynamic fanout 23,016.48 24,293.93 +5.55%
processRouteMasks static-heavy singleton dynamics 3,654.97 4,054.59 +10.93%
processRouteMasks sortable dynamic fanout 26,499.82 27,733.59 +4.66%
findRouteMatch decode mixed90 params uncached batch 29,618.76 40,791.57 +37.72%
findRouteMatch decode encoded params uncached batch 34,053.80 32,279.86 -5.21%

Notes:

  • Route construction improves most when the tree has many singleton dynamic arrays, because the full post-build traversal is removed.
  • Mostly-unencoded param extraction improves by avoiding unnecessary decoding.
  • Encoded-only params are the tradeoff case because the % check adds overhead before decoding.

Validation

  • tests/new-process-route-tree.test.ts: passed, 173 passed, no type errors.
  • Focused perf cases: passed.
  • git diff --check: clean.

Summary by CodeRabbit

  • Bug Fixes

    • Improved route parameter extraction, optional-parameter fallback behavior, trailing-slash handling, and matching accuracy
    • More efficient parameter decoding to avoid unnecessary work
    • Ensured nested route parse functions receive merged params in the correct order
  • Tests

    • Added performance benchmarks for route-tree processing
    • Expanded coverage for nested routes, wildcard/optional params, case-sensitivity, and malformed templates
  • Chores

    • Added scripts to run benchmark suites locally

Review Change Stack

@schiller-manuel schiller-manuel requested a review from Sheraff May 20, 2026 20:14
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

📝 Walkthrough

Walkthrough

Refactors route-tree parsing, construction, and matching: replaces helper-based segment parsing with inline char-code parsing, defers dynamic-child sorting via a sortables accumulator, adds conditional param decoding, tightens parse-validation and selection logic, and adds tests plus Vitest benchmarks and npm perf scripts.

Changes

Route-tree parsing, construction, and matching optimization

Layer / File(s) Summary
Segment parsing optimization
packages/router-core/src/new-process-route-tree.ts, packages/router-core/tests/optional-path-params.test.ts
parseSegment uses direct charCodeAt checks and inline brace/dollar parsing; removed brace-scanner helper. Tests validate parsing with non-zero start offsets.
Tree construction & deferred dynamic sorting
packages/router-core/src/new-process-route-tree.ts, packages/router-core/tests/match-params.test.ts
parseSegments threads a sortables accumulator to collect dynamic-child arrays for later sorting; unified dynamic-node creation; removed full-tree recursive sort pass; processRouteMasks/processRouteTree sort collected arrays; single-route parsing uses an empty sortables.
Parameter extraction and decoding
packages/router-core/src/new-process-route-tree.ts, packages/router-core/tests/new-process-route-tree.bench.ts
Introduces decodeParam to skip decoding when input contains no %; extractParams and fuzzy ** splats use decodeParam.
Matching and frame-selection refinements
packages/router-core/src/new-process-route-tree.ts, packages/router-core/tests/new-process-route-tree.test.ts
Rewrote trailing-slash detection and index-node fast-path to avoid removed helpers; validateParseParams assumes frame.node.parse present and returns null when parse returns false. Specificity/frame selection logic simplified.
Matching behavior tests and mask sorting
packages/router-core/tests/*
Adds tests for nested parent+child params.parse merging, optional-param parse fallback, wildcard suffix-only behavior, case-sensitivity fallback, parsed-sibling priority/fallback ordering, literal $ handling, malformed curly templates, and processRouteMasks specificity sorting.
Performance testing & benchmarks
packages/router-core/package.json, packages/router-core/tests/new-process-route-tree.bench.ts
Added test:perf and test:perf:dev scripts. New Vitest benchmark file generates synthetic route trees, verifies correctness, and measures preprocessing and uncached batch-matching performance across encoded/mixed inputs.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • TanStack/router#7411: Both PRs modify packages/router-core/src/new-process-route-tree.ts's dynamic segment child sorting via sortDynamic, affecting candidate ordering during match selection.

Suggested reviewers

  • Sheraff

Poem

🐇 I nibble bytes where segments grow,
char-codes hum and sortables flow,
params decoded, matches pruned,
tests and benches neatly tuned,
a rabbit cheers the router show!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'perf: optimize new-process-route-tree.ts' clearly and concisely summarizes the main objective of the pull request: performance optimizations to the new-process-route-tree.ts module.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf-process-route-tree

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown
Contributor

nx-cloud Bot commented May 20, 2026

View your CI Pipeline Execution ↗ for commit 3ea9836

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 2m 43s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-21 00:13:47 UTC

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 20, 2026

🚀 Changeset Version Preview

No changeset entries found. Merging this PR will not cause a version bump for any packages.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/router-core/tests/new-process-route-tree.bench.ts`:
- Around line 252-277: The benchmarks for processRouteMasks are using
masksProcessed (from makeRouteTree()) for every case, so staticHeavyMasks and
sortableFanoutMasks are not being measured against their matching processed
trees; fix by computing and passing the correct processedTree for each mask:
call processRouteTree(staticHeavyTree).processedTree for the static-heavy masks
benchmark and processRouteTree(sortableFanoutTree).processedTree for the
sortable dynamic fanout masks benchmark, and pass those results into
processRouteMasks instead of reusing masksProcessed (references:
processRouteMasks, masksProcessed, staticHeavyMasks, sortableFanoutMasks,
processRouteTree, staticHeavyTree, sortableFanoutTree).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 907cc122-3cd0-4fc2-abdd-e69027456d01

📥 Commits

Reviewing files that changed from the base of the PR and between 254cb88 and d64bf8d.

📒 Files selected for processing (6)
  • packages/router-core/package.json
  • packages/router-core/src/new-process-route-tree.ts
  • packages/router-core/tests/match-params.test.ts
  • packages/router-core/tests/new-process-route-tree.bench.ts
  • packages/router-core/tests/new-process-route-tree.test.ts
  • packages/router-core/tests/optional-path-params.test.ts

Comment on lines +252 to +277
const masksProcessed = processRouteTree(makeRouteTree()).processedTree
const decodeProcessed = processRouteTree(makeDecodeRouteTree()).processedTree
const encodedDecodePaths = makeDecodePaths(true)
const mixedDecodePaths = makeMixedDecodePaths()
let encodedDecodeIndex = 0
let mixedDecodeIndex = 0

bench('processRouteTree mixed tree', () => {
processRouteTree(routeTree)
})

bench('processRouteTree static-heavy singleton dynamics', () => {
processRouteTree(staticHeavyTree)
})

bench('processRouteTree sortable dynamic fanout', () => {
processRouteTree(sortableFanoutTree)
})

bench('processRouteMasks static-heavy singleton dynamics', () => {
processRouteMasks(staticHeavyMasks, masksProcessed)
})

bench('processRouteMasks sortable dynamic fanout', () => {
processRouteMasks(sortableFanoutMasks, masksProcessed)
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use matching processed trees for each processRouteMasks benchmark case.

staticHeavyMasks and sortableFanoutMasks are benchmarked against masksProcessed built from makeRouteTree(), so these two cases don't measure the intended tree/mask combinations.

Proposed fix
-  const masksProcessed = processRouteTree(makeRouteTree()).processedTree
+  const staticHeavyProcessed = processRouteTree(staticHeavyTree).processedTree
+  const sortableFanoutProcessed =
+    processRouteTree(sortableFanoutTree).processedTree
@@
   bench('processRouteMasks static-heavy singleton dynamics', () => {
-    processRouteMasks(staticHeavyMasks, masksProcessed)
+    processRouteMasks(staticHeavyMasks, staticHeavyProcessed)
   })
@@
   bench('processRouteMasks sortable dynamic fanout', () => {
-    processRouteMasks(sortableFanoutMasks, masksProcessed)
+    processRouteMasks(sortableFanoutMasks, sortableFanoutProcessed)
   })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/router-core/tests/new-process-route-tree.bench.ts` around lines 252
- 277, The benchmarks for processRouteMasks are using masksProcessed (from
makeRouteTree()) for every case, so staticHeavyMasks and sortableFanoutMasks are
not being measured against their matching processed trees; fix by computing and
passing the correct processedTree for each mask: call
processRouteTree(staticHeavyTree).processedTree for the static-heavy masks
benchmark and processRouteTree(sortableFanoutTree).processedTree for the
sortable dynamic fanout masks benchmark, and pass those results into
processRouteMasks instead of reusing masksProcessed (references:
processRouteMasks, masksProcessed, staticHeavyMasks, sortableFanoutMasks,
processRouteTree, staticHeavyTree, sortableFanoutTree).

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 20, 2026

Bundle Size Benchmarks

  • Commit: bc4031819d8a
  • Measured at: 2026-05-21T00:00:32.096Z
  • Baseline source: history:65b4abe65bc2
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Initial gzip Raw Brotli Trend
react-router.minimal 87.12 KiB -178 B (-0.20%) 86.99 KiB 272.91 KiB 75.76 KiB ███████▇▇▇▇▁
react-router.full 90.67 KiB -159 B (-0.17%) 90.53 KiB 284.41 KiB 78.71 KiB ███████▆▆▆▆▁
solid-router.minimal 35.40 KiB -131 B (-0.36%) 35.28 KiB 105.21 KiB 31.91 KiB ▇▇▇████▆▆▆▆▁
solid-router.full 40.11 KiB -165 B (-0.40%) 39.98 KiB 119.41 KiB 36.07 KiB ███████████▁
vue-router.minimal 53.18 KiB -148 B (-0.27%) 53.05 KiB 150.34 KiB 47.83 KiB ███████████▁
vue-router.full 58.30 KiB -154 B (-0.26%) 58.17 KiB 166.50 KiB 52.33 KiB ███████████▁
react-start.minimal 101.85 KiB -177 B (-0.17%) 101.71 KiB 321.37 KiB 88.17 KiB ▇▇▇████████▁
react-start.deferred-hydration 102.87 KiB -207 B (-0.20%) 101.99 KiB 323.01 KiB 89.15 KiB ████████▁
react-start.full 105.26 KiB -168 B (-0.16%) 105.12 KiB 331.68 KiB 91.02 KiB ███████▇▇▇▇▁
react-start.rsbuild.minimal 99.47 KiB -175 B (-0.17%) 99.29 KiB 315.85 KiB 85.62 KiB ███████████▁
react-start.rsbuild.full 102.74 KiB -190 B (-0.18%) 102.57 KiB 326.26 KiB 88.38 KiB ███████████▁
solid-start.minimal 49.52 KiB -159 B (-0.31%) 49.39 KiB 151.33 KiB 43.67 KiB ▇▇▇████▇▇▇▇▁
solid-start.deferred-hydration 53.59 KiB -151 B (-0.27%) 50.25 KiB 159.88 KiB 47.59 KiB ████▆▆▆▆▁
solid-start.full 55.31 KiB -152 B (-0.27%) 55.18 KiB 168.23 KiB 48.63 KiB ▇▇▇████████▁

Current gzip tracks all emitted client JS chunks. Initial gzip tracks only the entry/import graph. Trend sparkline is historical current gzip ending with this PR measurement; lower is better.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 20, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7449

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7449

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7449

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7449

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7449

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7449

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7449

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7449

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7449

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7449

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7449

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7449

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7449

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7449

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7449

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7449

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7449

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7449

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7449

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7449

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7449

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7449

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7449

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7449

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7449

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7449

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7449

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7449

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7449

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7449

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7449

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7449

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7449

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7449

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7449

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7449

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7449

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7449

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7449

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7449

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7449

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7449

commit: d77ce42

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 20, 2026

Merging this PR will not alter performance

✅ 5 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing perf-process-route-tree (d77ce42) with main (65b4abe)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

## Summary

- Optimized route-tree post-processing by sorting only dynamic child arrays that can need ordering.
- Applied the same sortable-array tracking to route masks.
- Added a param decode fast path that skips `decodeURIComponent` when a matched value has no `%` escape.
- Removed the recursive `sortTreeNodes` post-build tree walk.
- Pass `sortables` through recursive route parsing so nested dynamic arrays are recorded during construction.
- Skip sortable tracking for single-route lazy matching, where no sibling ordering can be needed.

## Bundle Size

Scenario: `react-router.minimal`

| Metric | Before | After | Delta |
| --- | ---: | ---: | ---: |
| gzip | 89,392 | 89,269 | -123 |
| initial gzip | 89,251 | 89,130 | -121 |
| raw | 280,592 | 279,600 | -992 |
| brotli | 77,753 | 77,670 | -83 |

## Focused Benchmarks

Benchmarks were compared against a baseline worktree with the same benchmark cases applied and only the implementation different.

| Case | Baseline hz | Current hz | Delta |
| --- | ---: | ---: | ---: |
| `processRouteTree static-heavy singleton dynamics` | 2,953.62 | 3,315.89 | +12.26% |
| `processRouteTree sortable dynamic fanout` | 23,016.48 | 24,293.93 | +5.55% |
| `processRouteMasks static-heavy singleton dynamics` | 3,654.97 | 4,054.59 | +10.93% |
| `processRouteMasks sortable dynamic fanout` | 26,499.82 | 27,733.59 | +4.66% |
| `findRouteMatch decode mixed90 params uncached batch` | 29,618.76 | 40,791.57 | +37.72% |
| `findRouteMatch decode encoded params uncached batch` | 34,053.80 | 32,279.86 | -5.21% |

Notes:

- Route construction improves most when the tree has many singleton dynamic arrays, because the full post-build traversal is removed.
- Mostly-unencoded param extraction improves by avoiding unnecessary decoding.
- Encoded-only params are the tradeoff case because the `%` check adds overhead before decoding.

## Validation

- `tests/new-process-route-tree.test.ts`: passed, `173 passed`, no type errors.
- Focused perf cases: passed.
- `git diff --check`: clean.
## Setup

BEFORE: clean `HEAD` worktree with only new tests/benchmarks applied.

AFTER: final route-tree implementation with `options` local restored after perf isolation.

Relevant logs:

- BEFORE worktree: `/var/folders/6f/2t42ntqs4yv4h6qwzbh5pmcm0000gn/T/opencode/route-tree-before-compare`
- AFTER worktree: `/var/folders/6f/2t42ntqs4yv4h6qwzbh5pmcm0000gn/T/opencode/route-tree-after-compare`
- Final main perf: `/tmp/route-tree-final-perf2.log`
- Final full bundle diff: `/tmp/route-tree-final-full-bundle-diff2.txt`

## Tests

| Check | BEFORE | AFTER |
| --- | --- | --- |
| focused unit | 243 passed | 243 passed |
| router-core types | passed | passed |
| route-tree perf bench | passed | passed |
| relevant e2e | not rerun in BEFORE worktree | 44 passed |
| whitespace | n/a | clean |

## Bundle Size

Direct comparison for this diff, `react-router.minimal`:

| Metric | BEFORE | AFTER | Delta |
| --- | ---: | ---: | ---: |
| gzip | 89269 | 89214 | -55 |
| initial gzip | 89130 | 89073 | -57 |
| raw | 279600 | 279460 | -140 |
| brotli | 77670 | 77582 | -88 |

Full benchmark vs unoptimized attribution baseline:

| Scenario | Gzip Delta |
| --- | ---: |
| react-router.full | -159 |
| react-router.minimal | -178 |
| react-start.deferred-hydration | -207 |
| react-start.full | -168 |
| react-start.minimal | -177 |
| react-start.rsbuild.full | -190 |
| react-start.rsbuild.minimal | -175 |
| solid-router.full | -165 |
| solid-router.minimal | -131 |
| solid-start.deferred-hydration | -151 |
| solid-start.full | -152 |
| solid-start.minimal | -159 |
| vue-router.full | -154 |
| vue-router.minimal | -148 |

## Perf

Matched full-run comparison, same tests/benches in temp worktrees:

| Bench | BEFORE hz | AFTER hz | Delta | Relevance |
| --- | ---: | ---: | ---: | --- |
| processRouteTree mixed tree | 8482.16 | 8406.43 | -0.9% | not relevant |
| processRouteTree static-heavy singleton dynamics | 3376.49 | 3414.27 | +1.1% | not relevant, high variance |
| processRouteTree sortable dynamic fanout | 25148.24 | 25787.98 | +2.5% | likely positive |
| processRouteTree parsed priority fanout | 46308.12 | 45775.45 | -1.2% | contradicted by focused rerun |
| processRouteTree optional fanout | 24307.28 | 25594.74 | +5.3% | likely positive, focused overlap |
| processRouteTree wildcard fanout | 38286.45 | 40198.57 | +5.0% | not reproduced by focused rerun |
| processRouteMasks static-heavy singleton dynamics | 3804.66 | 3709.58 | -2.5% | not relevant, overlaps |
| processRouteMasks sortable dynamic fanout | 27902.76 | 28855.50 | +3.4% | likely positive |
| findRouteMatch decode encoded params batch | 27156.51 | 30521.50 | +12.4% | not relevant, high RME |
| findRouteMatch decode plain params batch | 39202.70 | 42038.73 | +7.2% | not relevant, high RME |
| findRouteMatch decode mixed90 params batch | 38627.50 | 38124.96 | -1.3% | not relevant, high RME |
| findRouteMatch sortable dynamic fanout | 19710991.25 | 19006507.35 | -3.6% | not relevant, high variance |

Focused reruns used for suspected cases:

| Bench | BEFORE hz | AFTER hz | Delta | Relevance |
| --- | ---: | ---: | ---: | --- |
| processRouteTree parsed priority fanout | 47007.46 +/-0.32% | 48042.74 +/-0.29% | +2.2% | statistically relevant positive |
| processRouteTree optional fanout | 25094.35 +/-2.24% | 25718.38 +/-1.30% | +2.5% | not conclusive, ranges overlap |
| processRouteTree wildcard fanout | 39812.35 +/-1.79% | 39765.98 +/-3.07% | -0.1% | not relevant |
| processRouteTree static-heavy singleton dynamics | 3417.00 +/-3.12% | 3392.02 +/-0.77% | -0.7% | not relevant |

Static-heavy note: one narrow run showed a clear slowdown before restoring the `options` local. After restoring it, repeated matched runs no longer showed a stable regression; identical BEFORE runs varied more than the final AFTER delta.

## Conclusion

Final candidate keeps bundle wins and no statistically reliable perf regression remains. The only statistically relevant focused perf signal is positive for parsed priority fanout.
@schiller-manuel schiller-manuel force-pushed the perf-process-route-tree branch from 4fa429d to 3ea9836 Compare May 20, 2026 23:56
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/router-core/tests/new-process-route-tree.bench.ts (1)

326-367: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Benchmark input mismatch for processRouteMasks scenarios.

Line 326 builds masksProcessed from makeRouteTree(), then Lines 361-367 reuse it for staticHeavyMasks and sortableFanoutMasks. Those two cases are not measured against their matching processed trees, so the benchmark results are skewed.

Proposed fix
-  const masksProcessed = processRouteTree(makeRouteTree()).processedTree
+  const staticHeavyProcessed = processRouteTree(staticHeavyTree).processedTree
+  const sortableFanoutProcessedForMasks =
+    processRouteTree(sortableFanoutTree).processedTree
@@
   bench('processRouteMasks static-heavy singleton dynamics', () => {
-    processRouteMasks(staticHeavyMasks, masksProcessed)
+    processRouteMasks(staticHeavyMasks, staticHeavyProcessed)
   })
@@
   bench('processRouteMasks sortable dynamic fanout', () => {
-    processRouteMasks(sortableFanoutMasks, masksProcessed)
+    processRouteMasks(sortableFanoutMasks, sortableFanoutProcessedForMasks)
   })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/router-core/tests/new-process-route-tree.bench.ts` around lines 326
- 367, The benchmark uses masksProcessed =
processRouteTree(makeRouteTree()).processedTree for all mask tests, causing
mismatched inputs; update the two processRouteMasks calls to use processed trees
that match their masks by creating corresponding processedTree variables (e.g.,
const staticHeavyProcessed = processRouteTree(staticHeavyTree).processedTree and
const sortableFanoutProcessed =
processRouteTree(sortableFanoutTree).processedTree) and pass
staticHeavyProcessed to processRouteMasks(staticHeavyMasks, ...) and
sortableFanoutProcessed to processRouteMasks(sortableFanoutMasks, ...), leaving
other benchmarks unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@packages/router-core/tests/new-process-route-tree.bench.ts`:
- Around line 326-367: The benchmark uses masksProcessed =
processRouteTree(makeRouteTree()).processedTree for all mask tests, causing
mismatched inputs; update the two processRouteMasks calls to use processed trees
that match their masks by creating corresponding processedTree variables (e.g.,
const staticHeavyProcessed = processRouteTree(staticHeavyTree).processedTree and
const sortableFanoutProcessed =
processRouteTree(sortableFanoutTree).processedTree) and pass
staticHeavyProcessed to processRouteMasks(staticHeavyMasks, ...) and
sortableFanoutProcessed to processRouteMasks(sortableFanoutMasks, ...), leaving
other benchmarks unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7bc6c833-1305-4842-b733-d6d435fd96ba

📥 Commits

Reviewing files that changed from the base of the PR and between 4fa429d and d77ce42.

📒 Files selected for processing (6)
  • packages/router-core/package.json
  • packages/router-core/src/new-process-route-tree.ts
  • packages/router-core/tests/match-params.test.ts
  • packages/router-core/tests/new-process-route-tree.bench.ts
  • packages/router-core/tests/new-process-route-tree.test.ts
  • packages/router-core/tests/optional-path-params.test.ts

Copy link
Copy Markdown
Contributor

@nx-cloud nx-cloud Bot left a comment

Choose a reason for hiding this comment

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

Important

At least one additional CI pipeline execution has run since the conclusion below was written and it may no longer be applicable.

Nx Cloud has identified a possible root cause for your failed CI:

We were unable to attribute this E2E failure to the changes in this PR. The failing test (client side navigating to a route with scripts) lives in tanstack-solid-start-e2e-basic, a project not touched by this PR, and the script-injection behavior it exercises has no direct relationship to the route-tree sorting or param-decode optimizations introduced here. We recommend re-running the pipeline to determine whether this is a transient environment issue.

No code changes were suggested for this issue.

Trigger a rerun:

Rerun CI

Nx Cloud View detailed reasoning on Nx Cloud ↗


🎓 Learn more about Self-Healing CI on nx.dev

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant