Skip to content

perf(router-core): avoid quadratic cache resource handoff - #8240

Merged
Sheraff merged 2 commits into
mainfrom
codex/cache-resource-handoff
Sep 4, 2026
Merged

perf(router-core): avoid quadratic cache resource handoff#8240
Sheraff merged 2 commits into
mainfrom
codex/cache-resource-handoff

Conversation

@Sheraff

@Sheraff Sheraff commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

🎯 Changes

Committing a navigation currently checks each previous match against an array containing both active and cached matches. With many cached preloads, resource handoff repeatedly scans the cache even when its entries retain the same owners.

Use the cache's existing map to exclude retained match identities and matches without loader resources before handoff. The remaining owners are checked against active matches only, removing the quadratic cache membership scan. Filtering stays after publication and uses the captured cache so synchronous reentry preserves existing behavior. No public API changes; commitMatches is exported only from its internal module for tests and benchmarks.

Add ownership tests for retained identities, cloned committed matches, shared flights, same-ID replacements, duplicate owners, abort ordering, synchronous cache mutation/replacement, and fresh versus expired preloads. Add focused commit and public navigation benchmarks.

Performance

Local macOS measurements using Node 25.8.1 and Vitest/jsdom, comparing the baseline with identical benchmark support against the final implementation. Each operation below performs two complete router-core navigations with preloading outside the timer; these timings exclude React rendering and browser painting.

Cached preloads Before After Speedup Relative margin of error, before / after
0 44.58 µs 42.38 µs 5.23% / 2.71%
100 62.42 µs 59.52 µs 1.83% / 1.94%
1,000 409.49 µs 247.04 µs 1.66× 0.82% / 0.74%
5,000 5,293.04 µs 1,183.00 µs 4.47× 1.60% / 1.05%

Small-cache results do not establish a meaningful speedup. A focused commit benchmark with 5,000 retained preload resources improved from 2.35 ms to 0.253 ms (about 9.3×, both margins of error below 0.8%). The focused benchmark stubs abort controllers to isolate ownership work; unit tests cover real abort events. Benchmarks also cover empty caches and full, mixed, and zero retention. Cache reconstruction and active-match checks still run.

Tradeoff: if every cached entry expires while still owning a loader resource, the extra filtering pass can slow bulk cleanup. The focused comparison measured 38.6 → 46.8 µs for 1,000 expired resources and 208 → 225 µs for 5,000. Ordinary empty-cache checks showed only nanosecond-scale differences. The implementation keeps one filtering path without an empty-cache special case.

Bundle size

A full build of all 18 scenarios measured −3 to +9 gzip bytes versus the original baseline, with +19 raw bytes per scenario and unchanged chunk counts. React Router minimal increases by 3 gzip bytes (85,752 → 85,755); full increases by 2 gzip bytes (89,340 → 89,342). The large-cache runtime gains justify this small increase.

Validation

  • Router-core: 109 test files, 1,630 tests passed, four expected failures.
  • React Router: 77 test files, 1,037 tests passed, one skipped.
  • Both packages passed ESLint and type checks on TypeScript 5.6, 5.7, 5.8, 5.9, 6.0, and 7.0.
  • React basic application: all 24 Playwright Chromium tests passed.
  • Formatting, diff checks, and the full bundle-size benchmark passed.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested code changes locally with the relevant test commands, or tests do not apply to this pull request.
  • I fully understand the code in this pull request, including any code generated with AI assistance.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Performance

    • Improved navigation performance for routes with many cached matches by reducing unnecessary cache handoff scanning.
    • Retained cached resources more efficiently during navigation.
  • Bug Fixes

    • Improved handling of resource ownership when routes are retained, replaced, or removed from the cache.
    • Preserved expected preload behavior during unrelated navigation.
  • Tests

    • Added coverage for cache commits, resource ownership, replacements, expirations, and preload scenarios.
    • Added benchmarks for large caches and navigation performance.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 4, 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-04T20:39:59.834599Z 9be38ff PR opened
ℹ️ 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.

@nx-cloud

nx-cloud Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 9be38ff

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

☁️ Nx Cloud last updated this comment at 2026-09-04 20:49:11 UTC

@coderabbitai

coderabbitai Bot commented Sep 4, 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: 9e0ca917-c015-4f30-a511-faf0a6a38e54

📥 Commits

Reviewing files that changed from the base of the PR and between ee28348 and 9be38ff.

📒 Files selected for processing (5)
  • .changeset/every-vans-stand.md
  • packages/router-core/src/load-client.ts
  • packages/router-core/tests/cache-commit.bench.ts
  • packages/router-core/tests/cache-commit.test.ts
  • packages/router-core/tests/cache-navigation.bench.ts

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


📝 Walkthrough

Walkthrough

commitMatches now limits resource handoff to departing flight owners. New tests cover cache ownership and preload behavior. New benchmarks measure commit maintenance and unrelated navigation across cache sizes.

Changes

Cache resource handoff

Layer / File(s) Summary
Resource handoff and cache ownership
packages/router-core/src/load-client.ts, packages/router-core/tests/cache-commit.test.ts, .changeset/every-vans-stand.md
commitMatches is exported and transfers resources only from departing owners. Tests cover retained identities, replacements, publication-time mutations, cleanup, and preload behavior.
Commit maintenance benchmark
packages/router-core/tests/cache-commit.bench.ts
Benchmarks commit and cache maintenance across cache sizes, retention fractions, and flight families.
Navigation cache benchmark
packages/router-core/tests/cache-navigation.bench.ts
Benchmarks unrelated replace navigations after preloading item routes at multiple cache sizes.

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

Merge Risk: ⚪ Minimal · up to 9be38

This change improves navigation performance with large preload caches while preserving cached loader-flight ownership and cleanup behavior. The covered cache and preload scenarios indicate no remaining merge-readiness risk.

🚥 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%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 4 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 and concisely describes the main change: improving router-core cache resource handoff performance by avoiding quadratic scans.
Description check ✅ Passed The description follows the required template, explains the motivation and implementation, documents performance and validation results, and includes the required changeset. One checklist item remains…
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: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 4 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 codex/cache-resource-handoff

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.

@github-actions

github-actions Bot commented Sep 4, 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

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: b17ed167fec0
  • Measured at: 2026-09-04T20:40:12.591Z
  • Baseline source: history:ee283480dfa5
  • 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.7 KiB
+3 B
83.6 KiB
+4 B
262.0 KiB
+19 B
72.8 KiB
-129 B
██████▅▅▅▅▁▁
react-router.full 87.2 KiB
+2 B
87.1 KiB
+2 B
273.7 KiB
+19 B
76.0 KiB
+10 B
██████▅▅▅▆▁▁
solid-router.minimal 33.1 KiB
+8 B
33.0 KiB
+7 B
96.1 KiB
+19 B
29.8 KiB
+5 B
██████▆▆▆▆▁▂
solid-router.full 37.9 KiB
+4 B
37.8 KiB
+4 B
110.7 KiB
+19 B
34.1 KiB
-24 B
██████▅▆▆▅▁▂
vue-router.minimal 49.4 KiB
+6 B
49.3 KiB
+5 B
138.1 KiB
+19 B
44.7 KiB
+102 B
██████▇▆▆▇▁▂
vue-router.full 55.1 KiB
+6 B
54.9 KiB
+5 B
156.3 KiB
+19 B
49.6 KiB
+36 B
██████▇▆▆▇▁▂
react-start.minimal 96.7 KiB
+7 B
96.5 KiB
+7 B
304.3 KiB
+19 B
83.8 KiB
+143 B
██████▅▅▅▅▁▂
react-start.query-integration 104.0 KiB
+5 B
103.9 KiB
+8 B
330.8 KiB
+19 B
90.1 KiB
+45 B
███▅▅▅▅▁▂
react-start.deferred-hydration 97.4 KiB
+4 B
96.5 KiB
+5 B
305.7 KiB
+19 B
84.5 KiB
+26 B
██████▅▅▅▅▁▁
react-start.full 99.8 KiB
+9 B
99.7 KiB
+7 B
314.0 KiB
+19 B
86.5 KiB
+69 B
██████▅▄▄▅▁▂
react-start.rsbuild.minimal 99.9 KiB
-3 B
99.8 KiB
-3 B
314.6 KiB
+19 B
86.2 KiB
-59 B
██████▅▅▅▅▁▁
react-start.rsbuild.minimal-iife 100.3 KiB
-3 B
100.2 KiB
-3 B
315.6 KiB
+19 B
86.6 KiB
+63 B
██████▅▅▅▅▁▁
react-start.rsbuild.full 103.3 KiB
-2 B
103.1 KiB
-2 B
324.7 KiB
+19 B
89.0 KiB
+23 B
██████▆▆▅▅▁▁
solid-start.minimal 46.0 KiB
+4 B
45.8 KiB
+3 B
137.2 KiB
+19 B
40.9 KiB
-17 B
██████▆▆▆▆▁▂
solid-start.deferred-hydration 49.0 KiB
+2 B
45.9 KiB
+5 B
144.7 KiB
+19 B
43.6 KiB
-40 B
██████▆▅▅▆▁▁
solid-start.full 51.0 KiB
+1 B
50.9 KiB
+5 B
152.6 KiB
+19 B
45.2 KiB
-85 B
██████▆▆▆▇▁▁
vue-start.minimal 65.6 KiB
+5 B
65.5 KiB
+6 B
189.0 KiB
+19 B
58.4 KiB
+125 B
██████▆▇▇▇▁▂
vue-start.full 69.4 KiB
+7 B
69.3 KiB
+5 B
201.3 KiB
+19 B
61.6 KiB
-120 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.

@pkg-pr-new

pkg-pr-new Bot commented Sep 4, 2026

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

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

@tanstack/eslint-plugin-router

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

@tanstack/eslint-plugin-start

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-rsc

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-fn-stubs

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

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

@tanstack/vue-start-client

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

@tanstack/vue-start-server

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

@tanstack/zod-adapter

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

commit: 9be38ff

@codspeed-hq

codspeed-hq Bot commented Sep 4, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 7.58%

⚠️ 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
❌ 10 (👁 10) regressed benchmarks
✅ 159 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory mem server error-paths redirect (solid) 518.3 KB 366.3 KB +41.52%
Memory mem server peak-large-page (vue) 1.2 MB 1 MB +15.75%
Memory mem server error-paths redirect (react) 345.7 KB 303.9 KB +13.77%
Memory mem client unique-location-churn (vue) 501.1 KB 446.2 KB +12.32%
Memory mem server serialization-payload (react) 4.5 MB 4.1 MB +10.52%
Memory mem client unique-location-churn (solid) 252.9 KB 229.6 KB +10.15%
Memory mem client preload-churn (vue) 802 KB 762.3 KB +5.2%
Memory mem client navigation-churn (vue) 1.6 MB 1.6 MB +4.44%
Memory mem client interrupted-navigations (vue) 367.2 KB 356.3 KB +3.08%
Memory mem server aborted-requests (react) 840.7 KB 816.1 KB +3.02%
Memory mem server aborted-requests (solid) 1.3 MB 1.2 MB +3.01%
👁 Memory mem server serialization-payload (solid) 4.5 MB 4.6 MB -3.53%
👁 Memory mem server error-paths not-found (vue) 485.8 KB 2,002.8 KB -75.75%
👁 Simulation client-async-pipeline navigation loop (solid) 116.3 ms 122.5 ms -5.05%
👁 Simulation client-control-flow navigation loop (solid) 106.2 ms 112.3 ms -5.4%
👁 Simulation client-async-pipeline navigation loop (vue) 71.2 ms 77.1 ms -7.58%
👁 Simulation client-control-flow navigation loop (vue) 60.1 ms 65.6 ms -8.33%
👁 Simulation client-control-flow navigation loop (react) 106.9 ms 113 ms -5.44%
👁 Memory mem server error-paths not-found (react) 388.5 KB 425.5 KB -8.7%
👁 Memory mem server error-paths unmatched (react) 409.5 KB 863.8 KB -52.6%
... ... ... ... ... ...

ℹ️ 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 codex/cache-resource-handoff (9be38ff) with main (ee28348)

Open in CodSpeed

@Sheraff
Sheraff merged commit 2f91503 into main Sep 4, 2026
26 checks passed
@Sheraff
Sheraff deleted the codex/cache-resource-handoff branch September 4, 2026 20:55
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