Skip to content

optimize bundle-size & perf#7053

Open
schiller-manuel wants to merge 3 commits intomainfrom
chore-perf-bundlesize
Open

optimize bundle-size & perf#7053
schiller-manuel wants to merge 3 commits intomainfrom
chore-perf-bundlesize

Conversation

@schiller-manuel
Copy link
Copy Markdown
Contributor

@schiller-manuel schiller-manuel commented Mar 26, 2026

Summary by CodeRabbit

  • Refactor

    • Improved navigation lifecycle dispatch to better distinguish leaving, staying, and entering routes; streamlined error-state detection and route-matching logic.
  • New Features

    • Added a public check to detect error states among active routes.
  • Tests

    • Added a test ensuring same-route navigations with changed loader params are treated as a "stay" (no leave/enter).

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

📝 Walkthrough

Walkthrough

Refactored router internals: replaced several forEach usages with for...of, reworked load() lifecycle-hook dispatch to use pending/current routeId sets, centralized error-status checks via a new hasErrorMatch() helper, and adjusted hasNotFoundMatch() implementation.

Changes

Cohort / File(s) Summary
Core router refactor
packages/router-core/src/router.ts
Replaced multiple Set.forEach/Array.forEach with for...of loops across emit, cancelMatches, buildLocation, commitLocation, and state-prop comparison. Rewrote load() lifecycle-hook dispatch to compute pendingMatches/currentMatches and compare pendingRouteIds/activeRouteIds to call onEnter/onLeave/onStay. Added hasErrorMatch() and reimplemented hasNotFoundMatch() with explicit iteration; centralized HTTP status derivation.
Tests — lifecycle behavior
packages/router-core/tests/callbacks.test.ts
Added createControlledPromise helper import and a new onStay test verifying transitions to the same routeId (different match id) invoke onStay once and do not call onEnter/onLeave.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through loops from old to new,

Swapped forEach for for...of in view,
Hooks now check IDs, they stay or depart,
Errors gathered by one keen heart,
A rabbit cheers the router's smart.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is partially related to the changeset. While it mentions optimization goals, it does not clearly summarize the main technical changes (loop refactoring, hook dispatch simplification, error status computation centralization). Consider a more specific title that describes the primary changes, such as 'Refactor navigation lifecycle hooks and simplify loop patterns' or 'Optimize hook dispatch logic and centralize error status computation'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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 chore-perf-bundlesize

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

@nx-cloud
Copy link
Copy Markdown

nx-cloud bot commented Mar 26, 2026

View your CI Pipeline Execution ↗ for commit caf3888

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 53s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-26 23:23:30 UTC

@github-actions
Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 26, 2026

Bundle Size Benchmarks

  • Commit: 0e0a2817c300
  • Measured at: 2026-03-26T23:22:15.544Z
  • Baseline source: history:70b222513720
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Raw Brotli Trend
react-router.minimal 87.50 KiB -32 B (-0.04%) 275.94 KiB 76.11 KiB ████▇▇▃▃▁▁▁▁
react-router.full 90.79 KiB -31 B (-0.03%) 287.12 KiB 78.86 KiB ▇███▇▇▂▂▁▁▁▁
solid-router.minimal 35.53 KiB -38 B (-0.10%) 107.28 KiB 32.00 KiB ████▅▅▅▅▂▂▂▁
solid-router.full 40.00 KiB -43 B (-0.10%) 120.81 KiB 35.91 KiB ▆███▃▃▃▃▂▂▂▁
vue-router.minimal 53.40 KiB -19 B (-0.03%) 153.21 KiB 48.00 KiB ██████▁▁▁▁▁▁
vue-router.full 58.26 KiB -25 B (-0.04%) 168.67 KiB 52.22 KiB ▆█████▂▂▁▁▁▁
react-start.minimal 102.00 KiB -25 B (-0.02%) 324.12 KiB 88.32 KiB ▇███▆▆▂▂▁▁▁▁
react-start.full 105.38 KiB -31 B (-0.03%) 334.47 KiB 91.06 KiB ▇███▇▇▂▂▁▁▁▁
solid-start.minimal 49.63 KiB -25 B (-0.05%) 153.46 KiB 43.77 KiB ████▄▄▄▄▂▂▂▁
solid-start.full 55.14 KiB -26 B (-0.05%) 169.70 KiB 48.50 KiB ▅███▄▄▄▄▂▂▂▁

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

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 26, 2026

More templates

@tanstack/arktype-adapter

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

@tanstack/eslint-plugin-router

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-fn-stubs

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

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

@tanstack/vue-start-client

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

@tanstack/vue-start-server

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

@tanstack/zod-adapter

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

commit: caf3888

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.

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

2476-2503: Please lock the hook identity choice in with coverage.

This dispatch is now driven by routeId sets, while cache identity is still match.id. For transitions where the match instance changes but the routeId does not, the expected outcome is now onStay. A focused regression test here would make that contract explicit and protect future refactors.

🤖 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 2476 - 2503, Add a focused
regression test that locks the hook identity choice to routeId (not match.id) by
asserting that when a navigation changes the match instance but keeps the same
routeId the router invokes onStay (via looseRoutesById[...] .options.onStay)
instead of onLeave+onEnter; specifically, create a test that produces two
distinct Match objects with the same routeId (altering match.id or params),
trigger the transition that sets pendingRouteIds/activeRouteIds as in the
dispatch logic using currentMatches and pendingMatches, and verify onStay was
called once and onLeave/onEnter were not called, referencing looseRoutesById,
currentMatches, pendingMatches, pendingRouteIds and activeRouteIds in the test
to prevent regressions.
🤖 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 2476-2503: Add a focused regression test that locks the hook
identity choice to routeId (not match.id) by asserting that when a navigation
changes the match instance but keeps the same routeId the router invokes onStay
(via looseRoutesById[...] .options.onStay) instead of onLeave+onEnter;
specifically, create a test that produces two distinct Match objects with the
same routeId (altering match.id or params), trigger the transition that sets
pendingRouteIds/activeRouteIds as in the dispatch logic using currentMatches and
pendingMatches, and verify onStay was called once and onLeave/onEnter were not
called, referencing looseRoutesById, currentMatches, pendingMatches,
pendingRouteIds and activeRouteIds in the test to prevent regressions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 18316082-ea7c-4978-8d7d-2c2e3f8f2941

📥 Commits

Reviewing files that changed from the base of the PR and between 0e0a281 and 14ed429.

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

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Mar 26, 2026

Merging this PR will not alter performance

✅ 6 untouched benchmarks


Comparing chore-perf-bundlesize (caf3888) with main (70b2225)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (0e0a281) during the generation of this report, so 70b2225 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/router-core/src/router.ts (1)

2528-2534: ⚠️ Potential issue | 🟠 Major

Don’t classify caught navigation failures via active-match status.

hasErrorMatch() only looks at committed active matches. If something in the try block throws before a match is marked status: 'error'—for example a lifecycle hook or another non-match failure—this branch falls back to 200 even though navigation failed. Non-redirect/non-notFound failures should default to 500 here, with abort/cancel handled explicitly if you want those to stay neutral.

🛠️ Suggested fix
           const nextStatusCode = redirect
             ? redirect.status
             : notFound
               ? 404
-              : this.hasErrorMatch()
-                ? 500
-                : 200
+              : 500

Also applies to: 2933-2941

🤖 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 2528 - 2534, The status
selection for nextStatusCode incorrectly uses hasErrorMatch() (which checks only
committed active matches) and can return 200 when a non-match failure occurred;
change the logic in the nextStatusCode calculation (the redirect / notFound /
hasErrorMatch() branch) to treat any non-redirect and non-notFound failure as
500 by default, removing reliance on hasErrorMatch(); explicitly handle
abort/cancel cases if they should be neutral (e.g., check the specific
navigation failure type before setting a neutral status), and update the code
paths around nextStatusCode, redirect, notFound, and hasErrorMatch() so that
thrown errors during the try block lead to 500 rather than 200.
🧹 Nitpick comments (1)
packages/router-core/tests/callbacks.test.ts (1)

221-225: Avoid a fixed 10-tick poll in this test.

This depends on the pending match showing up within 10 Promise.resolve() turns. If store propagation slips by another tick, pendingFooMatch stays empty and the test becomes timing-dependent. Waiting on an explicit pending-match signal would make this deterministic.

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

In `@packages/router-core/tests/callbacks.test.ts` around lines 221 - 225, The
test currently polls pendingMatches via a fixed 10-tick loop (reading
router.stores.pendingMatchesSnapshot.state) which is timing-dependent; replace
that loop with an explicit await on the snapshot update by subscribing to
router.stores.pendingMatchesSnapshot and returning a Promise that resolves when
the snapshot's state becomes non-empty (unsubscribe immediately after
resolving). Target the pendingMatches variable and
router.stores.pendingMatchesSnapshot.subscribe/unsubscribe logic so the test
deterministically waits for the pending match instead of relying on
Promise.resolve() ticks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/router-core/src/router.ts`:
- Around line 2528-2534: The status selection for nextStatusCode incorrectly
uses hasErrorMatch() (which checks only committed active matches) and can return
200 when a non-match failure occurred; change the logic in the nextStatusCode
calculation (the redirect / notFound / hasErrorMatch() branch) to treat any
non-redirect and non-notFound failure as 500 by default, removing reliance on
hasErrorMatch(); explicitly handle abort/cancel cases if they should be neutral
(e.g., check the specific navigation failure type before setting a neutral
status), and update the code paths around nextStatusCode, redirect, notFound,
and hasErrorMatch() so that thrown errors during the try block lead to 500
rather than 200.

---

Nitpick comments:
In `@packages/router-core/tests/callbacks.test.ts`:
- Around line 221-225: The test currently polls pendingMatches via a fixed
10-tick loop (reading router.stores.pendingMatchesSnapshot.state) which is
timing-dependent; replace that loop with an explicit await on the snapshot
update by subscribing to router.stores.pendingMatchesSnapshot and returning a
Promise that resolves when the snapshot's state becomes non-empty (unsubscribe
immediately after resolving). Target the pendingMatches variable and
router.stores.pendingMatchesSnapshot.subscribe/unsubscribe logic so the test
deterministically waits for the pending match instead of relying on
Promise.resolve() ticks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f2aa5b93-44fd-4bb4-89ac-763d4bca7652

📥 Commits

Reviewing files that changed from the base of the PR and between 14ed429 and caf3888.

📒 Files selected for processing (2)
  • packages/router-core/src/router.ts
  • packages/router-core/tests/callbacks.test.ts

Comment on lines +2453 to 2455
} else {
exitingMatches = null
}
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.

else not necessary, exitingMatches is already null

Suggested change
} else {
exitingMatches = null
}
}

Comment on lines +2478 to +2481
const nextPendingRouteIds =
pendingRouteIds as Set<string> | null
const nextActiveRouteIds =
activeRouteIds as Set<string> | null
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.

This doesn't seem necessary, as casts to the same type those variables already have

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.

2 participants