Skip to content

Simplify loader flight and render acknowledgement ownership - #7926

Open
Sheraff wants to merge 3 commits into
fix-router-core-lane-match-loaderfrom
codex/simplify-match-loading-orchestration
Open

Simplify loader flight and render acknowledgement ownership#7926
Sheraff wants to merge 3 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. After the final automated formatting pass, the production diff is +153/-173 lines (net -20).

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.
  • Preload redirect continuation is owned by the preload's _preloads controller entry rather than an unrelated foreground _tx.
  • 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.
  • 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 correction moves the existing chunk wake-up earlier rather than adding another offer or authority.

The resulting authorities are deliberately narrow:

  • _tx owns foreground publication.
  • _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 lane runs its own uncached beforeLoad; its context and control flow remain lane-local.
  • 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 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. If the replacement supersedes an unacknowledged offer, its own exact acknowledgement establishes the minimum-visible deadline under the existing receipt rules.

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 four independent gates: lazy options, lazy pending-component preload, normal component preload, and loader completion. It proves that the default fallback remains until the lazy fallback is render-ready, then the lazy fallback replaces it while the normal component and loader are still pending. The first PR revision failed this contract in CI because it waited for the normal component too.
  • 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. Unlike the React receipt test, this is a demonstrated main regression.

Corrected to match supported behavior

  • The AbortController is not properly caught in parent loader #3928 rapid-successor test now 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 or private HMR _refreshRoute behavior.
  • 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

Compared with the exact base commit b8204ea9ad:

  • react-router.minimal: -67 B gzip, -65 B initial gzip, -227 B raw, and -73 B Brotli.
  • solid-router.minimal: -13 B gzip, -13 B initial gzip, -92 B raw, and +24 B Brotli.
  • All 17 bundle scenarios improve gzip by 6–88 B, with no JS chunk-count changes.
  • Summed scenario snapshots: -622 B gzip, -622 B initial gzip, -2,539 B raw, and -941 B Brotli.

The Solid CI correction costs 31 B gzip in react-router.minimal, 29 B in solid-router.minimal, and 598 B summed across the 17 scenarios relative to the first PR revision. Several equivalent phase implementations were measured and rejected; the retained callback-threaded version ties for the smallest gzip result that also waits for the pending component's own framework preload, while keeping the better Brotli result and clearer source. The complete PR remains smaller than the exact base in every gzip scenario.

Three individual scenario totals have small Brotli increases despite raw and gzip improvements; aggregate Brotli decreases and no chunks are added.

Focused runtime benchmarks on the first PR revision showed no regression signal. The lazy-phase follow-up moves an existing notification on a path those workloads do not exercise:

  • 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,513 passed, 3 expected failures.
  • React Router: 73 files, 987 passed, 1 skipped.
  • Solid Router: 54 client files with 837 passed and 1 skipped; 3 server files with 3 passed.
  • Vue Router: 54 files, 814 passed, 1 skipped.
  • The strengthened Solid lazy-pending regression passed explicitly on the final formatted source.
  • Focused orchestration coverage: 154/154 passed.
  • Router core, React, and Solid type tests passed with TypeScript 5.6, 5.7, 5.8, 5.9, 6.0, and 7.0; Vue's type suite passed 138/138.
  • ESLint completed with zero errors; existing warnings remain.
  • Automated formatting and git diff --check passed.
  • The comprehensive test and bundle checks passed on the final autofixed head.
  • 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 lazy-phase path is not exercised by those benchmarks, so no runtime patch was layered on from that signal.
  • Route head function can still execute before loader function finishes #6221 Vite SSR E2E: 1/1 passed.
  • Both React memory smoke runs passed all six workloads. These were correctness smokes, not memory measurements.

@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: b0808ee0-25fd-4944-8149-4b90e6ced8bf

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 626bd62

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

☁️ Nx Cloud last updated this comment at 2026-07-30 18:36:53 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: 24dc0c538e7d
  • Measured at: 2026-07-30T18:26:23.511Z
  • Baseline source: history:a3ee355b5934
  • 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.14 KiB -212 B (-0.24%) 87.00 KiB 269.24 KiB 75.87 KiB ███████████▁
react-router.full 90.93 KiB -131 B (-0.14%) 90.80 KiB 281.25 KiB 79.18 KiB ███████████▁
solid-router.minimal 34.90 KiB -642 B (-1.76%) 34.77 KiB 100.15 KiB 31.50 KiB ███████████▁
solid-router.full 40.01 KiB -588 B (-1.41%) 39.88 KiB 115.38 KiB 35.98 KiB ███████████▁
vue-router.minimal 52.20 KiB -815 B (-1.50%) 52.07 KiB 143.31 KiB 47.00 KiB ███████████▁
vue-router.full 58.16 KiB -795 B (-1.32%) 58.03 KiB 162.08 KiB 52.21 KiB ███████████▁
react-start.minimal 102.15 KiB +170 B (+0.16%) 102.01 KiB 318.39 KiB 88.68 KiB ▁▁▁▁▁▁▁▁▁▁▁█
react-start.deferred-hydration 102.89 KiB +169 B (+0.16%) 102.04 KiB 319.77 KiB 89.35 KiB ▁▁▁▁▁▁▁▁▁▁▁█
react-start.full 105.52 KiB +163 B (+0.15%) 105.38 KiB 328.39 KiB 91.49 KiB ▁▁▁▁▁▁▁▁▁▁▁█
react-start.rsbuild.minimal 99.88 KiB +187 B (+0.18%) 99.71 KiB 312.81 KiB 86.13 KiB ▁▁▁▁▁▁▁▁▁▂▂█
react-start.rsbuild.minimal-iife 100.28 KiB +187 B (+0.18%) 100.11 KiB 313.75 KiB 86.47 KiB ▁▁▁▁▁▁▁▁▁▂▂█
react-start.rsbuild.full 103.21 KiB +309 B (+0.29%) 103.04 KiB 322.96 KiB 88.89 KiB ▁▁▁▁▁▁▁▁▁▁▁█
solid-start.minimal 49.79 KiB +187 B (+0.37%) 49.66 KiB 148.19 KiB 44.19 KiB ▁▁▁▁▁▁▁▁▁▁▁█
solid-start.deferred-hydration 53.05 KiB +189 B (+0.35%) 49.72 KiB 156.23 KiB 47.23 KiB ▁▁▁▁▁▁▁▁▁▁▁█
solid-start.full 55.09 KiB -343 B (-0.60%) 54.96 KiB 163.98 KiB 48.75 KiB ███████████▁
vue-start.minimal 70.58 KiB -458 B (-0.63%) 70.45 KiB 201.22 KiB 62.70 KiB ███████████▁
vue-start.full 74.55 KiB -489 B (-0.64%) 74.42 KiB 213.84 KiB 66.11 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: 270f2f9

@codspeed-hq

codspeed-hq Bot commented Jul 30, 2026

Copy link
Copy Markdown

Merging this PR will regress 12 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

⚡ 11 improved benchmarks
❌ 12 regressed benchmarks
✅ 157 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 unmatched (react) 262.7 KB 604.3 KB -56.53%
Memory mem server serialization-payload (vue) 3.5 MB 6.1 MB -42.46%
Memory mem server error-paths not-found (vue) 329.3 KB 420.2 KB -21.62%
Memory mem client navigation-churn (react) 442 KB 542.2 KB -18.47%
Memory mem server error-paths not-found (solid) 414.9 KB 459.4 KB -9.7%
Simulation ssr server-fn not-found (solid) 61.8 ms 66.8 ms -7.5%
Memory mem client navigation-churn (solid) 497 KB 536.2 KB -7.3%
Simulation ssr server-fn POST (solid) 64.7 ms 69.5 ms -6.9%
Memory mem server peak-large-page (vue) 784.5 KB 835.4 KB -6.09%
Simulation ssr server-fn send-context (solid) 65.9 ms 69.6 ms -5.34%
Simulation ssr server-fn GET (solid) 70.6 ms 74.3 ms -4.88%
Memory mem server server-fn-churn (solid) 264.5 KB 274.8 KB -3.76%
Memory mem server server-fn-churn (vue) 4,139.7 KB 263.7 KB ×16
Memory mem server error-paths redirect (solid) 777.5 KB 283.1 KB ×2.7
Memory mem client unique-location-churn (solid) 598.8 KB 342.3 KB +74.95%
Memory mem client interrupted-navigations (react) 409.3 KB 251.6 KB +62.66%
Memory mem server serialization-payload (react) 5.2 MB 3.2 MB +61.84%
Memory mem server request-churn (react) 653 KB 498.1 KB +31.09%
Memory mem client interrupted-navigations (solid) 403.7 KB 340.6 KB +18.53%
Memory mem server error-paths redirect (vue) 351.7 KB 303.4 KB +15.89%
... ... ... ... ... ...

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

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 (270f2f9) with fix-router-core-lane-match-loader (b8204ea)

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