Skip to content

perf(router-core): share settle chain between superseded navigation waiters - #8222

Merged
Sheraff merged 1 commit into
mainfrom
fix/await-current-shared-settle
Sep 3, 2026
Merged

perf(router-core): share settle chain between superseded navigation waiters#8222
Sheraff merged 1 commit into
mainfrom
fix/await-current-shared-settle

Conversation

@Sheraff

@Sheraff Sheraff commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

awaitCurrent in load-client.ts looped await current.done and re-read router._tx after each completion. Under back-to-back navigations (anything that starts the next navigation in the same tick as onRendered, which includes the benchmarks/client-nav harness) every superseded waiter woke on every successor. A burst of N navigations cost N²/2 microtask iterations and kept every superseded transaction (matches, closures, controllers) alive until the router went idle.

Each transaction's done promise now follows its current successor, so every waiter on a transaction shares one chain instead of polling successors independently. Semantics are unchanged: a waiter resolves once the router has settled on a transaction other than its owner. A new test pins that contract for a same-tick burst.

Evidence

Instrumenting the built baseline benchmark bundle before the change: 15,867 navigations, 125,857,045 awaitCurrent loop iterations (= N²/2). In the CPU profiles this shows up as awaitCurrent at 20 to 42 percent self time plus (program) at 30 to 44 percent in the baseline, search-params, route-tree-scale and nested-params scenarios. Loaders and mount are unaffected because their timer hops let the chain settle.

Navigations per 10 seconds in benchmarks/client-nav (react, jsdom, same harness), measured by patching the built bundle:

scenario before after
baseline 15.4K 43.2K
route-tree-scale 17.9K 98.2K
search-params 16.5K 54.8K

Real-world impact is bounded to navigation bursts within a tick, but the retained transactions were a leak until idle, and the CodSpeed client-nav numbers were dominated by this, so any perf work benchmarked after this lands should be re-baselined.

Test plan

  • packages/router-core: vitest (107 files, 1613 passed), tsc, eslint
  • packages/react-router: vitest (77 files, 1037 passed)
  • New navigation-burst-settle.test.ts
  • Bundle size (gzip, benchmarks/bundle-size): react-router.minimal -1 B, react-router.full +3 B (vs main)
  • CodSpeed client-nav run on this PR

🤖 Generated with Claude Code

https://claude.ai/code/session_01C1tX2n8xegVBsZqoJPu7iv

Summary by CodeRabbit

  • Bug Fixes
    • Improved navigation settling during rapid successive navigations.
    • Superseded navigation requests now wait for the latest navigation to finish before resolving.
    • Prevented intermediate navigation waiters from completing prematurely or being awakened repeatedly.
    • Ensured navigation promises consistently resolve with the router in an idle state and displaying the final destination.

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: f273ccba-5511-4975-b4f4-237efab80e21

📥 Commits

Reviewing files that changed from the base of the PR and between 37877da and 8c76f33.

📒 Files selected for processing (3)
  • .changeset/await-current-shared-settle.md
  • packages/router-core/src/load-client.ts
  • packages/router-core/tests/navigation-burst-settle.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The router now chains load transactions to their current successors. Awaiting any transaction waits for the successor chain to settle. A regression test covers 25 same-tick navigations and the final idle state.

Changes

Transaction settlement

Layer / File(s) Summary
Successor-chain load completion
packages/router-core/src/load-client.ts, .changeset/await-current-shared-settle.md
awaitCurrent follows successor transactions. LoadTransaction.done includes successor settlement. loadClientRoute awaits the combined completion promise.
Same-tick navigation burst regression
packages/router-core/tests/navigation-burst-settle.test.ts
The test verifies that 25 navigation promises resolve at /items/24 with idle router status and successful route matches.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 8c76f

Superseded navigation promises now share successor completion and resolve once the final navigation settles, with regression coverage for same-tick bursts. No current merge-blocking risk remains.

Sequence Diagram(s)

sequenceDiagram
  participant Navigation
  participant loadClientRoute
  participant LoadTransaction
  participant awaitCurrent
  Navigation->>loadClientRoute: start navigation load
  loadClientRoute->>LoadTransaction: await tx[5]
  LoadTransaction->>awaitCurrent: follow successor chain
  awaitCurrent->>LoadTransaction: await successor completion
  LoadTransaction-->>loadClientRoute: resolve after chain settles
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: sharing the settle chain between superseded navigation waiters in router-core.
Description check ✅ Passed The description is detailed and directly related to the changes. It explains the quadratic behavior, the implementation, performance evidence, test coverage, and the pending CodSpeed check. It uses he…
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.
Full details: Description check

Explanation

The description is detailed and directly related to the changes. It explains the quadratic behavior, the implementation, performance evidence, test coverage, and the pending CodSpeed check. It uses headings that differ from the repository template and does not explicitly include the template checklist or release-impact section, but the required information is mostly present.

Full details: Docstring Coverage

Explanation

Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/await-current-shared-settle

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@nx-cloud

nx-cloud Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 8c76f33

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

☁️ Nx Cloud last updated this comment at 2026-09-03 13:01:20 UTC

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

5 package(s) bumped directly, 18 bumped as dependents.

🟩 Patch bumps

Package Version Reason
@tanstack/react-router 1.170.32 → 1.170.33 Changeset
@tanstack/router-core 1.171.27 → 1.171.28 Changeset
@tanstack/solid-router 1.170.30 → 1.170.31 Changeset
@tanstack/start-plugin-core 1.171.39 → 1.171.40 Changeset
@tanstack/vue-router 1.170.29 → 1.170.30 Changeset
@tanstack/react-start 1.168.49 → 1.168.50 Dependent
@tanstack/react-start-client 1.168.30 → 1.168.31 Dependent
@tanstack/react-start-rsc 0.1.48 → 0.1.49 Dependent
@tanstack/react-start-server 1.167.37 → 1.167.38 Dependent
@tanstack/router-cli 1.167.33 → 1.167.34 Dependent
@tanstack/router-generator 1.167.33 → 1.167.34 Dependent
@tanstack/router-plugin 1.168.35 → 1.168.36 Dependent
@tanstack/router-vite-plugin 1.167.35 → 1.167.36 Dependent
@tanstack/solid-start 1.168.47 → 1.168.48 Dependent
@tanstack/solid-start-client 1.168.29 → 1.168.30 Dependent
@tanstack/solid-start-server 1.167.36 → 1.167.37 Dependent
@tanstack/start-client-core 1.170.27 → 1.170.28 Dependent
@tanstack/start-server-core 1.169.31 → 1.169.32 Dependent
@tanstack/start-static-server-functions 1.167.32 → 1.167.33 Dependent
@tanstack/start-storage-context 1.167.29 → 1.167.30 Dependent
@tanstack/vue-start 1.168.46 → 1.168.47 Dependent
@tanstack/vue-start-client 1.167.32 → 1.167.33 Dependent
@tanstack/vue-start-server 1.167.36 → 1.167.37 Dependent

@pkg-pr-new

pkg-pr-new Bot commented Sep 2, 2026

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

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

@tanstack/eslint-plugin-router

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

@tanstack/eslint-plugin-start

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-rsc

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-fn-stubs

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

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

@tanstack/vue-start-client

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

@tanstack/vue-start-server

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

@tanstack/zod-adapter

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

commit: 8c76f33

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: 7bf42fe76465
  • Measured at: 2026-09-03T12:51:32.280Z
  • Baseline source: history:37877da166fe
  • Dashboard: bundle-size history

The following scenarios have bundle-size changes compared with the baseline:

Scenario Current (gzip) Initial (gzip) Raw Brotli Trend
react-router.minimal 83.8 KiB
-1 B
83.6 KiB
+4 B
262.1 KiB
-11 B
72.9 KiB
+19 B
▇▇██████▂▁▁▁
react-router.full 87.3 KiB
+3 B
87.1 KiB
+5 B
273.9 KiB
-11 B
76.0 KiB
+35 B
▇▇██████▁▁▁▂
solid-router.minimal 33.1 KiB
-1 B
33.0 KiB
+1 B
96.2 KiB
-11 B
29.9 KiB
+57 B
▇▇██████▂▂▂▁
solid-router.full 38.0 KiB
-3 B
37.8 KiB
+2 B
110.9 KiB
-11 B
34.2 KiB
-7 B
▆▆██████▁▂▂▁
vue-router.minimal 49.5 KiB
+1 B
49.3 KiB
0 B
138.3 KiB
-11 B
44.7 KiB
-26 B
▅▅██████▃▁▁▂
vue-router.full 55.1 KiB
+5 B
55.0 KiB
+4 B
156.5 KiB
-11 B
49.7 KiB
+137 B
▄▄██████▄▁▁▄
react-start.minimal 96.7 KiB
+4 B
96.6 KiB
+3 B
304.4 KiB
-11 B
83.7 KiB
+42 B
▆▆██████▂▁▁▂
react-start.query-integration 104.0 KiB
+2 B
103.9 KiB
+4 B
330.9 KiB
-11 B
90.2 KiB
-24 B
▇██▁▁▁▁
react-start.deferred-hydration 97.4 KiB
+7 B
96.6 KiB
+1 B
305.8 KiB
-11 B
84.5 KiB
+25 B
▇▇██████▂▁▁▃
react-start.full 99.8 KiB
+3 B
99.7 KiB
+4 B
314.1 KiB
-11 B
86.6 KiB
+57 B
▇▇██████▂▁▁▂
react-start.rsbuild.minimal 100.0 KiB
+2 B
99.8 KiB
+2 B
314.7 KiB
-11 B
86.3 KiB
+27 B
▇▇██████▂▁▁▁
react-start.rsbuild.minimal-iife 100.4 KiB
+2 B
100.2 KiB
+2 B
315.7 KiB
-11 B
86.6 KiB
+106 B
▇▇██████▂▁▁▁
react-start.rsbuild.full 103.3 KiB
+2 B
103.1 KiB
+2 B
324.8 KiB
-11 B
88.8 KiB
-244 B
▇▇██████▃▃▁▁
solid-start.minimal 46.0 KiB
+2 B
45.9 KiB
+4 B
137.3 KiB
-11 B
40.9 KiB
-6 B
████████▁▁▁▂
solid-start.deferred-hydration 49.1 KiB
+7 B
45.9 KiB
+7 B
144.8 KiB
-11 B
43.7 KiB
-28 B
▆▆██████▄▁▁▄
solid-start.full 51.1 KiB
+5 B
50.9 KiB
+5 B
152.7 KiB
-11 B
45.3 KiB
+47 B
▆▆██████▃▁▁▄
vue-start.minimal 65.6 KiB
0 B
65.5 KiB
+2 B
189.1 KiB
-11 B
58.4 KiB
+27 B
▃▃██████▁▃▃
vue-start.full 69.5 KiB
+5 B
69.3 KiB
+4 B
201.4 KiB
-11 B
61.7 KiB
+11 B
▃▃██████▁▁▁▃

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.

@codspeed-hq

codspeed-hq Bot commented Sep 2, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 3.82%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 11 improved benchmarks
❌ 12 (👁 12) regressed benchmarks
✅ 157 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory mem server error-paths redirect (vue) 891.9 KB 415.9 KB ×2.1
Memory mem server error-paths not-found (solid) 698.7 KB 563.1 KB +24.08%
Memory mem client preload-churn (vue) 919.3 KB 760.7 KB +20.85%
Memory mem client navigation-churn (vue) 1.7 MB 1.5 MB +12%
Simulation client-nested-params navigation loop (react) 151.2 ms 137 ms +10.33%
Memory mem client navigation-churn (solid) 649.4 KB 605.7 KB +7.21%
Memory mem server error-paths unmatched (react) 441.5 KB 413.9 KB +6.66%
Simulation ssr global-mw document (solid) 366.2 ms 353 ms +3.74%
Simulation ssr server-fn multipart (solid) 144.3 ms 139.5 ms +3.48%
Memory mem server request-churn (solid) 733.2 KB 709.4 KB +3.35%
Memory mem server serialization-payload (solid) 4.7 MB 4.5 MB +3.19%
👁 Memory mem server aborted-requests (vue) 996.3 KB 1,057.1 KB -5.75%
👁 Simulation client-async-pipeline navigation loop (react) 61.9 ms 64.5 ms -4.02%
👁 Memory mem server aborted-requests (solid) 1.2 MB 1.2 MB -4.51%
👁 Memory mem server server-fn-churn (solid) 336.7 KB 360.2 KB -6.54%
👁 Memory mem server aborted-requests (react) 844.5 KB 943.3 KB -10.48%
👁 Memory mem server error-paths error (react) 985 KB 1,019.3 KB -3.36%
👁 Memory mem server error-paths not-found (react) 436.4 KB 456.7 KB -4.45%
👁 Memory mem server error-paths redirect (react) 315.9 KB 325.9 KB -3.07%
👁 Memory mem server request-churn (react) 639.3 KB 688.9 KB -7.21%
... ... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Tip

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


Comparing fix/await-current-shared-settle (8c76f33) with main (37877da)

Open in CodSpeed

…e chain

`awaitCurrent` looped `await current.done` and re-read `router._tx` after
each completion. Under back-to-back navigations (anything that starts the
next navigation in the same tick as `onRendered`, including the client-nav
benchmarks) every superseded waiter woke on every successor, so a burst of
N navigations cost N²/2 microtask iterations and kept every superseded
transaction alive until the router went idle.

Each transaction's `done` promise now follows its current successor, so
every waiter on a transaction shares the same chain instead of polling
successors independently. A waiter still resolves once the router has
settled on a transaction other than its owner.

Measured in benchmarks/client-nav (react, navigations per 10s, jsdom):
baseline 15.4K -> 43K, route-tree-scale 17.9K -> 98K,
search-params 16.5K -> 55K. The instrumented baseline run showed
125.8M loop iterations for 15,867 navigations before the change.
Bundle: react-router.minimal -1 B gzip vs main.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1tX2n8xegVBsZqoJPu7iv
@Sheraff
Sheraff force-pushed the fix/await-current-shared-settle branch from 11997c8 to 8c76f33 Compare September 3, 2026 12:48
@Sheraff
Sheraff marked this pull request as ready for review September 3, 2026 12:55
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-03T12:58:39.523484Z 8c76f33 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Sheraff
Sheraff merged commit edf0e16 into main Sep 3, 2026
26 checks passed
@Sheraff
Sheraff deleted the fix/await-current-shared-settle branch September 3, 2026 13:10
@github-actions github-actions Bot mentioned this pull request Sep 3, 2026
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