Skip to content

Simplify loader flight and render acknowledgement ownership - #7926

Merged
Sheraff merged 8 commits into
fix-router-core-lane-match-loaderfrom
codex/simplify-match-loading-orchestration
Aug 2, 2026
Merged

Simplify loader flight and render acknowledgement ownership#7926
Sheraff merged 8 commits into
fix-router-core-lane-match-loaderfrom
codex/simplify-match-loading-orchestration

Conversation

@Sheraff

@Sheraff Sheraff commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

This stacks on #7805 and simplifies its match-loading rewrite around one rule: each match ID has at most one discoverable loader generation, each flight has one normalized outcome, and lane ownership decides where that outcome may publish.

The change closes the supported orchestration races selected from #7911 without adding another flag, counter, deadline, or completion promise. The latest follow-up continues that direction: _preflight identity is the sole preflight authority, standalone preload retirement has one finally, and refresh rollback belongs to the refresh transaction instead of the router.

The production source diff against the base is now +208/-223 lines (net -15).

Architecture

  • Loader work is claimed synchronously after a lane completes beforeLoad contextualization, before the async frame yields. This removes the zero-owner handoff gap without a retry loop.
  • Consumers already joined to a loader flight share its outcome, including error and notFound.
  • A non-success flight is removed from discovery before normalization callbacks run. Existing consumers retain it through their leases, while a later navigation or an onError-reentrant navigation starts a fresh generation.
  • _preflight identity alone decides whether navigation preflight or hydration work is current. Replacement authority is installed before the previous controller is aborted, so abort-driven reentrancy cannot revive old work.
  • Standalone preloads register once, execute once, and retire through one finally that removes their _preloads authority, transfers match resources, and aborts their public signal. A redirect continues only while that authority was still active.
  • React render completion acknowledges the exact offered matches-array reference through one router-owned tuple. This replaces structural match signatures and component-local acknowledgement state.
  • Development refresh rollback is owned by the refresh transaction. React invokes the router transition inside the React action and propagates action failures, allowing failed HMR publications to roll back instead of committing their error UI.
  • Lazy option installation, pending-component readiness, and normal-component readiness are distinct phases. An active client lane reuses its existing pending-offer callback once a newly installed pendingComponent finishes its own preload; route success still waits for the normal component.

The resulting authorities are deliberately narrow:

  • _preflight owns preflight and hydration currentness.
  • _tx owns foreground publication and any refresh rollback.
  • _preloads owns speculative cancellation and private redirect continuation.
  • The loader-flight registry controls discovery.
  • Match leases control loader lifetime.
  • The exact React receipt controls render acknowledgement.

The lazy pending callback is internal. RouterCore.loadRouteChunk retains its previous two-argument public type.

Intentional semantics and compromises

  • A provider owns one router for its lifetime. Router swapping and separate Activity/unmount completion machinery are intentionally not supported.
  • Every client lane runs its own uncached beforeLoad; its context and control flow remain lane-local.
  • Hydration is the exception: transported server matches are trusted, so client context() and beforeLoad do not rerun while hydrating them.
  • Loader results are cached, and discovery is single-flight per match ID. A joined loader can therefore publish a value produced with the context of the lane that started it.
  • shouldReload: true requires a loader generation but does not bypass discoverable same-ID work. The active-flight overlap is otherwise undefined; using the uniform rule preserves the relevant main behavior and removes bytes.
  • A navigation that has joined a pending preload consumes that loader's shared error or notFound. A navigation planned after the terminal flight has retired retries normally.
  • invalidate() reruns planning, route context(), and beforeLoad, but does not introduce matches owned only by an active speculative preload as a second invalidation/admission authority. Same-ID loader work owned only by that preload can remain discoverable and reusable, including its old-context value.
  • Replacing a default pending fallback with a lazy route fallback keeps the same pending session and absolute reveal deadline. It does not restart pendingMs or rebase a pendingMinMs deadline that an exact render acknowledgement has already established.

How #7911 was integrated

#7911, the independent audit, and the proposed mechanism were used as sources of candidate specifications and reproductions, not adopted wholesale.

Adopted or reformulated

  • Pending preload error and notFound are shared with an already-joined navigation. The final tests use deterministic public-API gates.
  • A failed pending parent preload remains shared while independently started descendant work can settle.
  • A later navigation retries after a completed failed preload while retaining reusable successful ancestor loader data.
  • A standalone preload redirect continues privately and cannot overwrite an unrelated navigation error.
  • The background-successor probe was reduced to a public-API test proving that a successor does not fall back to older cached data while a generation is in flight. It intentionally does not constrain whether the successor joins that generation or starts a later one.
  • React has public Suspense coverage proving that a suspended same-membership publication cannot acknowledge its successor. Exact-reference acknowledgement is the implementation used to enforce that contract.
  • The Router defaultPendingComponent Not effective(version:1.121.24) #4467 Solid lazy-pending test is retained as a public phase contract and strengthened with independent gates for lazy options, lazy pending-component preload, normal component preload, and loader completion.
  • The Route head function can still execute before loader function finishes #6221 Vite SSR E2E verifies body/head consistency after browser back and reproduces the demonstrated main regression where the body is fresh but the head/title remains stale.
  • A terminal standalone preload aborts the loader signal exposed through the public loader API.
  • A loader error remains authoritative when the normal component chunk fails later; the late chunk failure cannot replace the boundary error or emit a second onError.
  • Concurrent component preloads share one underlying lazy import without specifying promise identity as public behavior.
  • Public HMR E2Es cover failed-publication rollback and recovery, refresh redirects preserving retained loader data and local state, and refresh waiting for a pending navigation.

Corrected to match supported behavior

  • The AbortController is not properly caught in parent loader #3928 rapid-successor test preserves main semantics: one active same-ID parent loader may serve multiple successors, including its old-context value, while loaderDeps-keyed child loaders supersede normally.
  • Same-generation failures normalize once for joined consumers.
  • A navigation started from onError receives a fresh generation because the terminal flight is retired before callbacks.
  • A superseded terminal reservation cannot poison a successor delayed in beforeLoad.

Intentionally not adopted

  • Provider router swapping.
  • A second React Activity/unmount completion authority.
  • Forcing shouldReload: true to duplicate active same-ID loader work.
  • Turning invalidation into a separate active-preload admission/cancellation model.
  • A guarantee that loaders always observe or publish the newest beforeLoad context.
  • Mask coverage that already passed on main, redundant cases, and probes that required mutating internals.
  • The broader unrelated SSR, protocol, and framework fixes explored by wip for fix-router-core-lane-match-loader #7911.

Size and performance

For react-router.minimal, combining the exact-base measurement with the final-head follow-up gives:

  • Base b8204ea9ad: 89,298 B gzip.
  • Final head: 89,211 B gzip.
  • Total: -87 B gzip, -88 B initial gzip, -296 B raw, and -28 B Brotli.

The latest follow-up alone changed that scenario from 89,231 B to 89,211 B gzip (-20 B), with -23 B initial gzip and -69 B raw. Measured independently, sole _preflight authority accounted for about -9 B gzip and centralized preload retirement for the remaining -11 B; the development-only HMR ownership and error-propagation changes had zero production impact.

The preceding head's complete 17-scenario sweep improved every scenario's gzip size by 6–88 B, with no JS chunk-count changes. The final follow-up was remeasured on the representative React scenario rather than repeating the complete sweep. Its small +45 B Brotli movement is compression-layout noise against lower raw and gzip sizes; the final scenario remains 28 B below the exact base in Brotli.

Focused runtime benchmarks on the first PR revision showed no regression signal:

  • Base navigation, async pipeline, and mount deltas were within RME (-0.16%, -0.11%, and +0.24% throughput).
  • Loader throughput improved 1.13% with mean time down 1.12%.
  • Preload throughput improved 1.85% with mean time down 1.81%.

Validation

  • Router core: 104 files, 1,515 passed, 3 expected failures.
  • React Router: 73 files, 988 passed, 1 skipped.
  • Solid Router: 54 client files with 837 passed and 1 skipped; 3 server files with 3 passed on the preceding head.
  • Vue Router: 54 files, 814 passed, 1 skipped on the preceding head.
  • The latest follow-up's focused coverage passed: 8 core assertions, all 4 component-preload tests, 3 HMR E2Es, 61 preflight/hydration tests, 46 preload/lease tests, 37 core HMR/loader tests, and 9 React publication tests.
  • Router core, React, Solid, and Vue type suites passed on the latest head. The preceding CI matrix also passed Router core, React, and Solid with TypeScript 5.6 through 7.0, plus Vue's 138 type assertions.
  • Router core and React ESLint completed with zero errors; existing warnings remain.
  • Automated formatting and git diff --check passed.
  • Route head function can still execute before loader function finishes #6221 Vite SSR E2E: 1/1 passed on the preceding head.
  • Both React memory smoke runs passed all six workloads on the preceding head. These were correctness smokes, not memory measurements.
  • CodSpeed's external analysis remains report-only red. Its own result warns that the comparison used different runtime environments and shows large bidirectional memory swings across unrelated workloads; the final authority-only paths are not exercised by those benchmarks, so no runtime patch was layered on from that signal.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ccfc0e69-c3b4-4e0b-8091-e4b6658350fd

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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 Jul 30, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit bcde937

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

☁️ Nx Cloud last updated this comment at 2026-08-02 12:13:59 UTC

@Sheraff
Sheraff marked this pull request as ready for review July 30, 2026 16:18
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

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

🟩 Patch bumps

Package Version Reason
@tanstack/react-router 1.170.18 → 1.170.19 Changeset
@tanstack/router-core 1.171.15 → 1.171.16 Changeset
@tanstack/solid-router 1.170.18 → 1.170.19 Changeset
@tanstack/vue-router 1.170.17 → 1.170.18 Changeset
@tanstack/react-start 1.168.32 → 1.168.33 Dependent
@tanstack/react-start-client 1.168.16 → 1.168.17 Dependent
@tanstack/react-start-rsc 0.1.31 → 0.1.32 Dependent
@tanstack/react-start-server 1.167.22 → 1.167.23 Dependent
@tanstack/router-cli 1.167.21 → 1.167.22 Dependent
@tanstack/router-generator 1.167.21 → 1.167.22 Dependent
@tanstack/router-plugin 1.168.23 → 1.168.24 Dependent
@tanstack/router-vite-plugin 1.167.23 → 1.167.24 Dependent
@tanstack/solid-start 1.168.32 → 1.168.33 Dependent
@tanstack/solid-start-client 1.168.16 → 1.168.17 Dependent
@tanstack/solid-start-server 1.167.22 → 1.167.23 Dependent
@tanstack/start-client-core 1.170.14 → 1.170.15 Dependent
@tanstack/start-plugin-core 1.171.24 → 1.171.25 Dependent
@tanstack/start-server-core 1.169.17 → 1.169.18 Dependent
@tanstack/start-static-server-functions 1.167.19 → 1.167.20 Dependent
@tanstack/start-storage-context 1.167.17 → 1.167.18 Dependent
@tanstack/vue-start 1.168.31 → 1.168.32 Dependent
@tanstack/vue-start-client 1.167.19 → 1.167.20 Dependent
@tanstack/vue-start-server 1.167.22 → 1.167.23 Dependent

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: f6388c97d72d
  • Measured at: 2026-08-02T12:03:18.287Z
  • Baseline source: history:821295080766
  • 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 87.10 KiB -251 B (-0.28%) 86.96 KiB 269.12 KiB 75.87 KiB ███████████▁
react-router.full 90.91 KiB -161 B (-0.17%) 90.77 KiB 281.13 KiB 79.14 KiB ███████████▁
solid-router.minimal 34.86 KiB -678 B (-1.86%) 34.74 KiB 100.03 KiB 31.44 KiB ███████████▁
solid-router.full 39.97 KiB -630 B (-1.52%) 39.84 KiB 115.26 KiB 35.98 KiB ███████████▁
vue-router.minimal 52.15 KiB -865 B (-1.59%) 52.02 KiB 143.19 KiB 46.95 KiB ███████████▁
vue-router.full 58.12 KiB -840 B (-1.39%) 57.99 KiB 161.96 KiB 52.08 KiB ███████████▁
react-start.minimal 102.00 KiB +14 B (+0.01%) 101.86 KiB 318.11 KiB 88.51 KiB ▁▁▁▁▁▁▁▁▁▁▁█
react-start.deferred-hydration 102.74 KiB +22 B (+0.02%) 101.89 KiB 319.48 KiB 89.27 KiB ▁▁▁▁▁▁▁▁▁▁▁█
react-start.full 105.40 KiB +36 B (+0.03%) 105.26 KiB 328.11 KiB 91.40 KiB ▁▁▁▁▁▁▁▁▁▁▁█
react-start.rsbuild.minimal 99.77 KiB +78 B (+0.08%) 99.60 KiB 312.54 KiB 86.09 KiB ▁▁▁▁▁▁▂▂▂▂▂█
react-start.rsbuild.minimal-iife 100.17 KiB +76 B (+0.07%) 100.00 KiB 313.47 KiB 86.38 KiB ▁▁▁▁▁▁▂▂▂▂▂█
react-start.rsbuild.full 103.10 KiB +199 B (+0.19%) 102.93 KiB 322.68 KiB 88.81 KiB ▁▁▁▁▁▁▁▁▁▁▁█
solid-start.minimal 49.69 KiB +88 B (+0.17%) 49.57 KiB 147.90 KiB 44.15 KiB ▁▁▁▁▁▁▁▁▁▁▁█
solid-start.deferred-hydration 52.96 KiB +92 B (+0.17%) 49.63 KiB 155.94 KiB 47.12 KiB ▁▁▁▁▁▁▁▁▁▁▁█
solid-start.full 54.97 KiB -462 B (-0.81%) 54.84 KiB 163.69 KiB 48.64 KiB ███████████▁
vue-start.minimal 70.47 KiB -568 B (-0.78%) 70.35 KiB 200.95 KiB 62.59 KiB ███████████▁
vue-start.full 74.44 KiB -609 B (-0.79%) 74.31 KiB 213.55 KiB 66.06 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

pkg-pr-new Bot commented Jul 30, 2026

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

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

@tanstack/eslint-plugin-router

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

@tanstack/eslint-plugin-start

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-rsc

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-fn-stubs

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

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

@tanstack/vue-start-client

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

@tanstack/vue-start-server

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

@tanstack/zod-adapter

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

commit: bcde937

@codspeed-hq

codspeed-hq Bot commented Jul 30, 2026

Copy link
Copy Markdown

Merging this PR will regress 7 benchmarks

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

⚡ 10 improved benchmarks
❌ 7 regressed benchmarks
✅ 163 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory mem server error-paths not-found (solid) 414.9 KB 790.1 KB -47.49%
Memory mem client navigation-churn (react) 442 KB 542.1 KB -18.45%
Memory mem client navigation-churn (solid) 497 KB 573.4 KB -13.32%
Simulation ssr server-fn during document ssr (react) 63.8 ms 67.2 ms -5.14%
Memory mem server error-paths not-found (react) 248.5 KB 260.7 KB -4.67%
Simulation ssr selective (solid) 70.2 ms 73 ms -3.78%
Memory mem server error-paths not-found (vue) 329.3 KB 339.9 KB -3.11%
Memory mem server server-fn-churn (vue) 4,139.7 KB 274.3 KB ×15
Memory mem server error-paths redirect (solid) 777.5 KB 283.4 KB ×2.7
Memory mem client interrupted-navigations (react) 409.3 KB 251.5 KB +62.74%
Memory mem server serialization-payload (react) 5.2 MB 3.2 MB +61.57%
Memory mem server request-churn (react) 653 KB 497.6 KB +31.22%
Memory mem client interrupted-navigations (solid) 403.7 KB 340.6 KB +18.53%
Memory mem server error-paths redirect (vue) 351.7 KB 304.1 KB +15.62%
Memory mem client unique-location-churn (solid) 598.8 KB 531.2 KB +12.73%
Memory mem client interrupted-navigations (vue) 542 KB 493.6 KB +9.81%
Memory mem server error-paths redirect (react) 195.3 KB 185.2 KB +5.48%

Tip

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


Comparing codex/simplify-match-loading-orchestration (bcde937) with fix-router-core-lane-match-loader (b8204ea)

Open in CodSpeed

@Sheraff
Sheraff merged commit 00d68c4 into fix-router-core-lane-match-loader Aug 2, 2026
25 of 26 checks passed
@Sheraff
Sheraff deleted the codex/simplify-match-loading-orchestration branch August 2, 2026 13:38
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