Skip to content

refactor(router-core): router.load onReady match transition performance#6813

Open
Sheraff wants to merge 1 commit intomainfrom
refactor-router-core-ready-transition-performance
Open

refactor(router-core): router.load onReady match transition performance#6813
Sheraff wants to merge 1 commit intomainfrom
refactor-router-core-ready-transition-performance

Conversation

@Sheraff
Copy link
Contributor

@Sheraff Sheraff commented Mar 3, 2026

Basic "variable re-ordering" to skip some computation when possible in router > load > loadMatches > onReady > startTransition

Summary by CodeRabbit

  • Refactor
    • Optimized internal routing system logic for improved performance.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

The change modifies the router's commitLocation flow to conditionally handle pending/mounted matches. A mountPending flag derived from pendingMatches length now guards the computation of exiting/entering/staying matches, with lifecycle hook execution adapted to safely handle nullable arrays through optional chaining and type casting.

Changes

Cohort / File(s) Summary
Router Lifecycle Match Processing
packages/router-core/src/router.ts
Introduces conditional logic using a mountPending flag to guard computation of exiting/entering/staying matches. Makes previously non-nullable match arrays nullable; modifies how cachedMatches are extended based on mounting state; adapts lifecycle hook execution (onLeave, onEnter, onStay) to safely iterate over possibly-null arrays.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

package: router-core

Suggested reviewers

  • schiller-manuel

Poem

🐰 When matches are pending, the router hops with care,
Computing only what's mounted—no wasteful layers to spare,
Nullable arrays now dance through the lifecycle flow,
With optional chaining, the safe path's the way to go! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title mentions 'router.load onReady match transition performance' but the actual changes focus on conditional handling for pending/mounted matches during commitLocation flow and optimizing match calculations—the title's specific method references don't align with the core refactoring. Consider revising the title to better reflect the main change, such as 'refactor(router-core): optimize match calculations with conditional pending/mounted logic' or similar.
✅ Passed checks (2 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-router-core-ready-transition-performance

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

@nx-cloud
Copy link

nx-cloud bot commented Mar 3, 2026

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit cd6db1f

Command Status Duration Result
nx run @benchmarks/client-nav:test:perf:solid ❌ Failed 1m 15s View ↗
nx run @benchmarks/ssr:test:perf:vue ✅ Succeeded 2m 10s View ↗
nx run @benchmarks/ssr:test:perf:solid ✅ Succeeded 1m 54s View ↗
nx run @benchmarks/ssr:test:perf:react ✅ Succeeded 2m 3s View ↗
nx run @benchmarks/client-nav:test:perf:vue ✅ Succeeded 1m 24s View ↗
nx run @benchmarks/client-nav:test:perf:react ✅ Succeeded 7m 19s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-03 18:10:24 UTC

@github-actions
Copy link

github-actions bot commented Mar 3, 2026

Bundle Size Benchmarks

  • Commit: 2217f7c3f19f
  • Measured at: 2026-03-03T17:50:46.478Z
  • Baseline source: history:2217f7c3f19f
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Raw Brotli Trend
react-router.minimal 86.62 KiB +47 B (+0.05%) 272.48 KiB 75.24 KiB ▁▁▁▁▁▁▁▁▁▁▁█
react-router.full 89.66 KiB +50 B (+0.05%) 282.82 KiB 77.94 KiB ▁▁▁▁▁▁▁▁▁▁▁█
solid-router.minimal 35.92 KiB +42 B (+0.11%) 107.59 KiB 32.26 KiB ▁▁▁▁▁▁▁▁▁▁▁█
solid-router.full 40.25 KiB +40 B (+0.10%) 120.65 KiB 36.13 KiB ▁▁▁▁▁▁▁▁▁▁▁█
vue-router.minimal 51.78 KiB +26 B (+0.05%) 147.57 KiB 46.57 KiB ▁▁▁▁▁▁▁▁▁▁▁█
vue-router.full 56.58 KiB +39 B (+0.07%) 163.16 KiB 50.83 KiB ▁▁▁▁▁▁▁▁▁▁▁█
react-start.minimal 99.15 KiB +37 B (+0.04%) 311.62 KiB 85.89 KiB ▁▁▁▁▁▁▁▁▁▁▁█
react-start.full 102.55 KiB +46 B (+0.04%) 321.42 KiB 88.65 KiB ▁▁▁▂▂▂▂▂▂▂▂█
solid-start.minimal 48.23 KiB +38 B (+0.08%) 145.17 KiB 42.68 KiB ▁▁▁▁▁▁▁▁▁▁▁█
solid-start.full 53.72 KiB +40 B (+0.07%) 161.11 KiB 47.35 KiB ▁▁▁▂▂▂▂▂▂▂▂█

Trend sparkline is historical gzip bytes ending with this PR measurement; lower is better.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 3, 2026

More templates

@tanstack/arktype-adapter

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

@tanstack/eslint-plugin-router

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-fn-stubs

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

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

@tanstack/vue-start-client

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

@tanstack/vue-start-server

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

@tanstack/zod-adapter

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

commit: 196d208

Copy link
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.

🧹 Nitpick comments (2)
packages/router-core/src/router.ts (2)

2427-2452: Precompute membership sets to remove repeated scans in hot transition path.

This block repeatedly calls .some(...) inside filters/loops. Precomputing Sets for IDs/routeIds keeps the commit path linear and better aligned with the PR’s performance goal.

⚡ Suggested refactor
-                      if (mountPending) {
-                        exitingMatches = currentMatches.filter(
-                          (match) =>
-                            !pendingMatches.some((d) => d.id === match.id),
-                        )
+                      if (mountPending) {
+                        const pendingIds = new Set(
+                          pendingMatches.map((d) => d.id),
+                        )
+                        const pendingRouteIds = new Set(
+                          pendingMatches.map((d) => d.routeId),
+                        )
+                        const currentRouteIds = new Set(
+                          currentMatches.map((d) => d.routeId),
+                        )
+
+                        exitingMatches = currentMatches.filter(
+                          (match) => !pendingIds.has(match.id),
+                        )
 
                         // Lifecycle-hook identity: routeId only (route presence in tree)
                         hookExitingMatches = currentMatches.filter(
-                          (match) =>
-                            !pendingMatches.some(
-                              (d) => d.routeId === match.routeId,
-                            ),
+                          (match) => !pendingRouteIds.has(match.routeId),
                         )
                         hookEnteringMatches = []
                         hookStayingMatches = []
                         for (const match of pendingMatches) {
-                          if (
-                            currentMatches.some(
-                              (d) => d.routeId === match.routeId,
-                            )
-                          ) {
+                          if (currentRouteIds.has(match.routeId)) {
                             hookStayingMatches.push(match)
                           } else {
                             hookEnteringMatches.push(match)
                           }
                         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-core/src/router.ts` around lines 2427 - 2452, The current
transition code repeatedly calls pendingMatches.some(...) inside filters/loops
(affecting exitingMatches, hookExitingMatches and the loop that builds
hookEnteringMatches/hookStayingMatches), causing repeated scans; precompute two
Sets (e.g., pendingIdSet and pendingRouteIdSet) from pendingMatches once before
the filters/loop and replace .some checks with pendingIdSet.has(match.id) and
pendingRouteIdSet.has(match.routeId) respectively so exitingMatches,
hookExitingMatches, hookEnteringMatches and hookStayingMatches are computed with
O(n) scans using currentMatches and pendingMatches only once each.

2413-2415: Eliminate type assertion/cast escapes by using default empty arrays instead of null.

In strict mode, exitingMatches! at line 2472 and (matches as null | Array<AnyRouteMatch>) at line 2493 bypass type narrowing. Refactor the hook variable declarations to initialize as empty arrays rather than null, which enables static type safety without assertions:

  • Change let hookExitingMatches: Array<AnyRouteMatch> | null = null and related declarations to use [] as defaults
  • For exitingMatches, ensure the variable is always assigned an array (empty or populated) so the ! assertion at line 2472 becomes unnecessary
  • This allows the forEach at line 2493 to work without the type cast

Applies to lines 2413–2415, 2472, and 2493.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/router-core/src/router.ts` around lines 2413 - 2415, The hook match
variables hookExitingMatches, hookEnteringMatches, and hookStayingMatches should
be initialized to empty arrays rather than null to avoid type-assertion escapes;
change their declarations from "Array<AnyRouteMatch> | null = null" to just
"Array<AnyRouteMatch> = []" (or let ... = []), ensure any use of exitingMatches
is always assigned an array (so remove the "exitingMatches!" non-null assertion
in the code that references it), and remove the "(matches as null |
Array<AnyRouteMatch>)" cast by ensuring matches is typed/handled as an array so
the forEach (and similar iterations) can operate without casts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/router-core/src/router.ts`:
- Around line 2427-2452: The current transition code repeatedly calls
pendingMatches.some(...) inside filters/loops (affecting exitingMatches,
hookExitingMatches and the loop that builds
hookEnteringMatches/hookStayingMatches), causing repeated scans; precompute two
Sets (e.g., pendingIdSet and pendingRouteIdSet) from pendingMatches once before
the filters/loop and replace .some checks with pendingIdSet.has(match.id) and
pendingRouteIdSet.has(match.routeId) respectively so exitingMatches,
hookExitingMatches, hookEnteringMatches and hookStayingMatches are computed with
O(n) scans using currentMatches and pendingMatches only once each.
- Around line 2413-2415: The hook match variables hookExitingMatches,
hookEnteringMatches, and hookStayingMatches should be initialized to empty
arrays rather than null to avoid type-assertion escapes; change their
declarations from "Array<AnyRouteMatch> | null = null" to just
"Array<AnyRouteMatch> = []" (or let ... = []), ensure any use of exitingMatches
is always assigned an array (so remove the "exitingMatches!" non-null assertion
in the code that references it), and remove the "(matches as null |
Array<AnyRouteMatch>)" cast by ensuring matches is typed/handled as an array so
the forEach (and similar iterations) can operate without casts.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2217f7c and cd6db1f.

📒 Files selected for processing (1)
  • packages/router-core/src/router.ts

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 3, 2026

Merging this PR will not alter performance

✅ 5 untouched benchmarks


Comparing refactor-router-core-ready-transition-performance (cd6db1f) with main (2217f7c)

Open in CodSpeed

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