Skip to content

perf(router-core): avoid async scaffolding for synchronous hooks - #8004

Open
Sheraff wants to merge 3 commits into
mainfrom
agent/optimize-sync-client-navigation
Open

perf(router-core): avoid async scaffolding for synchronous hooks#8004
Sheraff wants to merge 3 commits into
mainfrom
agent/optimize-sync-client-navigation

Conversation

@Sheraff

@Sheraff Sheraff commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • keep synchronous lifecycle hook values out of waitFor's Promise, AbortController, and abort-listener scaffolding
  • retain the existing cancellation path for Promise-returning hooks, with an explicit post-await currentness/abort check for synchronous beforeLoad results
  • document the architectural boundary and add focused adversarial tests plus repeatable client-navigation benchmarks

Why

Most client navigation hooks are typed as Awaitable<T>, but the internal bridge treated every result as Promise-like. Even entirely synchronous navigation therefore allocated a wrapper Promise/controller, installed and removed an abort listener, and crossed an extra microtask boundary.

This change makes the fast path part of the existing awaitable architecture: raw values remain raw, while actual Promises use the existing cancellation machinery. It intentionally does not add a generalized thenable adapter or a separate after-the-fact optimization layer.

Impact

Local A/B runs:

  • 80 synchronous waits: 0.0222 ms → 0.0035 ms (-84.2%, ~6.3× faster)
  • 10 navigations through 8 synchronous beforeLoad hooks: 0.6411 ms → 0.6223 ms (-2.9%)
  • existing nested-params client-nav benchmark: mean 5.7912 ms → 5.7436 ms (-0.82%), p75 -0.80%, p99 -1.80%
  • minimal React Router bundle: +3 B gzip; affected bundle scenarios ranged from +1 B to +8 B gzip

Full measurements and methodology are in RESULT-optimization-promise-controller-context.md.

Validation

  • router-core focused cancellation/adversarial tests: 26 passed
  • router-core unit suite: 1,539 passed, 3 expected failures
  • router-core type tests: TypeScript 5.6–7 passed
  • router-core ESLint: 0 errors (26 existing warnings)
  • React basic file-based redirect E2E: 33 Chromium cases passed
  • Prettier, git diff --check, and the affected bundle suite passed

Summary by CodeRabbit

  • Performance

    • Improved client-side navigation performance for synchronous route hooks.
    • Reduced unnecessary cancellation overhead when processing immediate results.
  • Bug Fixes

    • Prevented aborted navigations from continuing into loaders or accepting stale results.
    • Preserved cancellation handling for asynchronous operations.
  • Tests

    • Added coverage for synchronous and asynchronous cancellation behavior.
    • Added benchmarks for navigation scenarios across nested routes.

@coderabbitai

coderabbitai Bot commented Aug 8, 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e43acd53-7f08-4f41-ac23-2b7b844bb92e

📥 Commits

Reviewing files that changed from the base of the PR and between f97188f and 1ca28da.

📒 Files selected for processing (6)
  • RESULT-optimization-promise-controller-context.md
  • packages/router-core/INTERNALS.md
  • packages/router-core/src/load-client.ts
  • packages/router-core/tests/client-lane-adversarial.test.ts
  • packages/router-core/tests/client-load-sync.bench.ts
  • packages/router-core/tests/load-client-wait-for.test.ts

📝 Walkthrough

Walkthrough

The client loading path now handles synchronous values without cancellation listeners, rechecks cancellation after beforeLoad, adds adversarial coverage, and introduces benchmarks with correctness, bundle-size, type, lint, and E2E validation results.

Changes

Client-lane optimization

Layer / File(s) Summary
Synchronous waitFor handling
packages/router-core/src/load-client.ts, packages/router-core/tests/load-client-wait-for.test.ts
waitFor returns synchronous values directly. Promise inputs retain abort-aware handling and listener cleanup.
Contextualized cancellation checks
packages/router-core/src/load-client.ts, packages/router-core/INTERNALS.md, packages/router-core/tests/client-lane-adversarial.test.ts
beforeLoad rechecks the abort signal and lane currentness after completion. An adversarial test confirms that an aborted navigation does not run its loader.
Benchmark and validation coverage
packages/router-core/tests/client-load-sync.bench.ts, RESULT-optimization-promise-controller-context.md
The benchmark measures synchronous and asynchronous route work across parameterized navigations. The report records runtime, bundle-size, correctness, type, lint, and E2E results.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 1ca28

The change keeps synchronous navigation hooks on a faster path while preserving cancellation for asynchronous hooks; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant RouterNavigation
  participant beforeLoad
  participant AbortSignal
  participant RouteLoader
  RouterNavigation->>beforeLoad: Execute synchronously
  beforeLoad->>AbortSignal: Queue abort
  RouterNavigation->>AbortSignal: Check after beforeLoad
  RouterNavigation-->>RouteLoader: Skip loader when aborted
Loading

Possibly related PRs

  • TanStack/router#7805: Introduced the lane-based client loading implementation refined by this change.
  • TanStack/router#8067: Also changes contextualize in load-client.ts, but addresses retained ancestor context during pending navigation.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 clearly and concisely describes the main optimization: avoiding async scaffolding for synchronous router hooks.
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.
✨ 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 agent/optimize-sync-client-navigation

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 Aug 8, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 1ca28da

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

☁️ Nx Cloud last updated this comment at 2026-08-15 12:03:26 UTC

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

1 package(s) bumped directly, 4 bumped as dependents.

🟩 Patch bumps

Package Version Reason
@tanstack/react-router 1.170.29 → 1.170.30 Changeset
@tanstack/react-start 1.168.46 → 1.168.47 Dependent
@tanstack/react-start-client 1.168.27 → 1.168.28 Dependent
@tanstack/react-start-rsc 0.1.45 → 0.1.46 Dependent
@tanstack/react-start-server 1.167.34 → 1.167.35 Dependent

@pkg-pr-new

pkg-pr-new Bot commented Aug 8, 2026

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

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

@tanstack/eslint-plugin-router

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

@tanstack/eslint-plugin-start

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-rsc

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-fn-stubs

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

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

@tanstack/vue-start-client

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

@tanstack/vue-start-server

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

@tanstack/zod-adapter

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

commit: 1ca28da

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: 26aeebc3615f
  • Measured at: 2026-08-15T11:49:45.679Z
  • Baseline source: history:f97188fdb4c3
  • Dashboard: bundle-size history

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

Scenario Current (gzip) Delta vs baseline Initial gzip Raw Brotli Trend
react-router.minimal 83.83 KiB +4 B (+0.00%) 83.69 KiB 262.46 KiB 72.95 KiB ▂▂▂▃██▆▂▂▁▁▁
react-router.full 87.32 KiB -2 B (-0.00%) 87.18 KiB 274.17 KiB 76.04 KiB ▄▄▄▄██▅▂▂▁▁▁
solid-router.minimal 33.13 KiB +10 B (+0.03%) 33.00 KiB 96.16 KiB 29.93 KiB ▆▇████▅▁▁▁▁▂
solid-router.full 37.94 KiB +3 B (+0.01%) 37.82 KiB 110.80 KiB 34.15 KiB ▆▇████▅▁▁▁▁▁
vue-router.minimal 49.51 KiB +4 B (+0.01%) 49.38 KiB 138.34 KiB 44.68 KiB ▁█▇███▅▃▃▃▃▃
vue-router.full 55.12 KiB +4 B (+0.01%) 54.99 KiB 156.54 KiB 49.55 KiB ▁█████▆▄▄▄▄▅
react-start.minimal 96.63 KiB +8 B (+0.01%) 96.49 KiB 304.69 KiB 83.79 KiB ▄▄▄▄██▅▂▂▁▁▂
react-start.full 99.80 KiB +2 B (+0.00%) 99.67 KiB 314.41 KiB 86.43 KiB ▄▄▄▄██▄▁▁▁▁▁
react-start.rsbuild.minimal 99.97 KiB +6 B (+0.01%) 99.80 KiB 315.07 KiB 86.15 KiB ▁▁▁▂███▃▃▁▁▂
react-start.rsbuild.minimal-iife 100.38 KiB +8 B (+0.01%) 100.21 KiB 316.01 KiB 86.57 KiB ▁▁▁▂███▂▂▁▁▂
react-start.rsbuild.full 103.29 KiB +2 B (+0.00%) 103.12 KiB 325.16 KiB 88.86 KiB ▁▁▁▂███▂▂▁▁▁
solid-start.minimal 45.87 KiB +7 B (+0.01%) 45.75 KiB 137.22 KiB 40.84 KiB ▇█████▄▁▁▁▁▂
solid-start.deferred-hydration 48.96 KiB +5 B (+0.01%) 45.82 KiB 144.68 KiB 43.66 KiB ▇▇████▄▁▁▁▁▁
solid-start.full 50.97 KiB +4 B (+0.01%) 50.84 KiB 152.61 KiB 45.16 KiB ▆█████▄▁▁▁▁▁
vue-start.minimal 65.57 KiB +5 B (+0.01%) 65.44 KiB 189.16 KiB 58.35 KiB ▁█████▆▅▅▅▅▅
vue-start.full 69.38 KiB +2 B (+0.00%) 69.26 KiB 201.46 KiB 61.66 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.

@codspeed-hq

codspeed-hq Bot commented Aug 8, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 0.49%

⚠️ 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

⚡ 6 improved benchmarks
❌ 9 (👁 2) regressed benchmarks
✅ 165 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory mem server serialization-payload (react) 4.1 MB 5.6 MB -27.74%
Memory mem server request-churn (react) 657.9 KB 804.5 KB -18.22%
Memory mem client navigation-churn (vue) 1.5 MB 1.7 MB -10.81%
Memory mem server peak-large-page (vue) 1 MB 1.1 MB -6.6%
Memory mem client unique-location-churn (vue) 539.8 KB 577.5 KB -6.54%
Memory mem server error-paths not-found (react) 416.3 KB 436.9 KB -4.7%
Simulation ssr control-flow route headers (react) 302.4 ms 312 ms -3.06%
Memory mem server serialization-payload (solid) 7.4 MB 4.5 MB +66.47%
Memory mem client navigation-churn (solid) 960.6 KB 683.9 KB +40.47%
Memory mem server error-paths error (solid) 1,044.4 KB 974 KB +7.23%
Memory mem server peak-large-page (solid) 1.1 MB 1 MB +3.56%
Memory mem client interrupted-navigations (vue) 478 KB 461.8 KB +3.52%
👁 Memory mem server error-paths redirect (react) 309.3 KB 325 KB -4.86%
👁 Memory mem server error-paths redirect (vue) 403.5 KB 504.5 KB -20.03%
👁 Memory mem server server-fn-churn (vue) 379.5 KB 354.9 KB +6.92%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing agent/optimize-sync-client-navigation (1ca28da) with main (f97188f)

Open in CodSpeed

@Sheraff
Sheraff marked this pull request as ready for review August 15, 2026 11:49
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