Skip to content

perf(router-core): keep search middleware collection optimized - #8385

Merged
schiller-manuel merged 1 commit into
schiller-manuel-remove-build-location-sharingfrom
schiller-manuel-search-middleware-compile-memory
Sep 13, 2026
Merged

schiller-manuel merged 1 commit into
schiller-manuel-remove-build-location-sharingfrom
schiller-manuel-search-middleware-compile-memory

Conversation

@schiller-manuel

@schiller-manuel schiller-manuel commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #8382.

Mechanism

CodSpeed's memory instrument flagged the mem client interrupted-navigations peak-memory benchmark on the location-reuse changes (#8370): react +14.6%, vue +29.8%, solid +10.8% versus base fc31a1d. Under CodSpeed's worker flags (--no-opt --predictable, single threaded) that benchmark's peak is set by a synchronous Maglev compile that happens inside the measured run.

The function is getSearchMiddlewares in packages/router-core/src/router.ts. In both base and head it is compiled during the preparation calls and then deoptimized (wrong map) on the first named property load off a route's options. In base its only caller was the small applySearchMiddleware, which Maglev compiled right afterwards and inlined it into, so the callee never tiered up again. Since #8370, build calls getSearchMiddlewares directly and does not inline it, so it becomes hot again during the measured run and recompiles there (about four 32 KB zone segments), setting the peak.

Iterating destRoutes with a counted loop instead of for...of makes the function compile once during preparation and never deoptimize, so there is no recompilation in the measured window. Middleware order and live route-option reads are unchanged. Hoisting the option reads alone did not help (and made the peak worse). This builds on the earlier perf(router-core): reduce middleware compilation memory commit in this stack, which already separated getSearchMiddlewares from the recursive middleware execution for the same class of reason.

Diff

 // Keep this separate from recursive execution to limit JIT compiler memory.
+// A counted loop instead of `for...of`: Maglev's inlined array iteration
+// deoptimizes on route option shapes and recompiles later, whereas indexed
+// reads stay optimized once compiled.
 function getSearchMiddlewares(
   destRoutes: ReadonlyArray<AnyRoute>,
   includeValidateSearch: boolean | undefined,
 ) {
   const middlewares = [] as Array<SearchMiddleware<any>>

-  for (const route of destRoutes) {
-    const routeOptions = route.options
+  for (let i = 0; i < destRoutes.length; i++) {
+    const routeOptions = destRoutes[i]!.options
     if ('search' in routeOptions) {

Plus a @tanstack/router-core patch changeset.

Measurements

Peak outstanding malloc bytes in the measured run, reproduced locally with a malloc interposer under CodSpeed's exact worker flags (@codspeed/core getV8Flags() for analysis mode plus the repo's flags from benchmarks/memory/runtime.ts), mirroring the plugin's seven preparation calls. These are local macOS arm64 / Node 24.8.0 native-allocation measurements, not a Linux CodSpeed CI run. Results are byte-deterministic across builds.

framework base a7427d6 head 62ed0ce (#8370 tip) with this fix
react 248,880 287,760 (+15.6%) 248,944 (+0.03%)
vue 256,704 292,656 (+14.0%) 256,640 (−0.02%)
solid 287,408 288,240 (+0.3%) 287,632 (+0.08%)

Bundle

react-router.minimal measured with pnpm benchmark:bundle-size:run against a baseline taken at the stack top (f5c4a4d7ce):

metric baseline candidate delta
gzip 86,004 86,012 +8
initial gzip 85,864 85,874 +10
raw 267,901 267,916 +15
brotli 75,039 75,013 −26

Verification

  • @tanstack/router-core:test:unit — 133 files, 3303 passed, 4 expected fail
  • @tanstack/router-core:test:types — clean on all TypeScript versions (5.6–7.0)
  • @tanstack/router-core:test:eslint — 0 errors, 27 warnings. One warning is new and inherent to this change: @typescript-eslint/prefer-for-of at the counted loop. The rule is warning-level and the same file already carries an identical accepted warning for the counted loop in matchRoutesInternal (line 1774). No disable comment was added.
  • git diff --check and prettier on the changed files — clean

Summary by CodeRabbit

  • Bug Fixes
    • Improved navigation performance and memory usage by preventing unnecessary runtime recompilation when processing search middleware across navigations.
  • Release
    • Included a patch release for the router core package.

@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview 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: Advanced

Run ID: a703aea6-d6c2-4a77-bae7-99c71c4c496c

📥 Commits

Reviewing files that changed from the base of the PR and between f5c4a4d and 6aec14d.

📒 Files selected for processing (2)
  • .changeset/steady-loops-compile.md
  • packages/router-core/src/router.ts

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


📝 Walkthrough

Walkthrough

The search middleware collection now uses an indexed counted loop over destination routes. A changeset records a patch release for @tanstack/router-core and documents the JIT optimization rationale.

Changes

Search middleware optimization

Layer / File(s) Summary
Counted loop and release metadata
packages/router-core/src/router.ts, .changeset/steady-loops-compile.md
getSearchMiddlewares now uses indexed iteration without changing middleware behavior. The changeset declares a patch release and documents the optimization.

Priority: ⬇️ Low

Estimated code review effort: 1 (Trivial) | ~5 minutes

Change: Refactor

Suggested reviewers: sheraff

Merge Risk: ⚪ Minimal · up to 65b66

The indexed loop preserves the existing middleware behavior, and the PR is ready to merge after normal checks.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main performance optimization in router-core.
Description check ✅ Passed The description provides detailed change motivation, implementation details, benchmark results, verification results, and release impact. It does not reproduce the template checklist headings or check…
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. (1 skipped: 1 …
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
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch schiller-manuel-search-middleware-compile-memory

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.

@schiller-manuel
schiller-manuel added this pull request to stack #8346 September 11, 2026 23:47
@nx-cloud

nx-cloud Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 65b6654

Command Status Duration Result
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 58s View ↗

☁️ Nx Cloud last updated this comment at 2026-09-12 22:32:14 UTC

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: 1ddd64e5e656
  • Measured at: 2026-09-12T22:10:35.503Z
  • Baseline source: history:ae6853592904
  • 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 84.0 KiB
+18 B
83.9 KiB
+15 B
261.6 KiB
-695 B
73.3 KiB
+63 B
█████▁▁▁▁▁▁▂
react-router.full 87.5 KiB
+38 B
87.4 KiB
+40 B
273.3 KiB
-703 B
76.2 KiB
+53 B
█████▂▂▂▂▂▁▃
solid-router.minimal 33.4 KiB
-19 B
33.3 KiB
-19 B
95.9 KiB
-832 B
30.2 KiB
-8 B
▅█████████▆▁
solid-router.full 38.3 KiB
+11 B
38.2 KiB
+9 B
110.6 KiB
-831 B
34.5 KiB
+47 B
▂▅▅▅▅▅▅▅▅▅▁█
vue-router.minimal 49.5 KiB
-86 B
49.4 KiB
-84 B
137.1 KiB
-1.2 KiB
44.8 KiB
-83 B
█████▃▃▃▃▃▃▁
vue-router.full 55.1 KiB
-79 B
55.0 KiB
-78 B
155.3 KiB
-1.2 KiB
49.7 KiB
-111 B
█████▃▃▃▃▃▂▁
react-start.minimal 96.9 KiB
+58 B
96.8 KiB
+56 B
303.9 KiB
-690 B
84.1 KiB
+60 B
█████▁▁▁▁▁▁▄
react-start.query-integration 104.3 KiB
+45 B
104.1 KiB
+46 B
330.4 KiB
-698 B
90.4 KiB
+23 B
█████▁▁▁▁▁▁▄
react-start.deferred-hydration 97.7 KiB
+54 B
96.8 KiB
+54 B
305.3 KiB
-690 B
84.8 KiB
+21 B
█████▁▁▁▁▁▁▄
react-start.full 100.1 KiB
+48 B
100.0 KiB
+46 B
313.6 KiB
-709 B
86.8 KiB
+105 B
█████▁▁▁▁▁▁▃
react-start.rsbuild.minimal 100.2 KiB
+44 B
100.0 KiB
+44 B
314.3 KiB
-618 B
86.5 KiB
+64 B
█████▁▁▁▁▁▁▃
react-start.rsbuild.minimal-iife 100.6 KiB
+52 B
100.4 KiB
+52 B
315.2 KiB
-601 B
86.9 KiB
+152 B
█████▁▁▁▁▁▁▄
react-start.rsbuild.full 103.5 KiB
+67 B
103.3 KiB
+67 B
324.3 KiB
-614 B
89.2 KiB
-4 B
█████▁▁▁▁▁▁▄
solid-start.minimal 46.4 KiB
+39 B
46.3 KiB
+37 B
137.1 KiB
-832 B
41.2 KiB
+42 B
▃▂▂▂▂▂▂▂▂▂▁█
solid-start.deferred-hydration 49.4 KiB
+20 B
46.3 KiB
+21 B
144.4 KiB
-830 B
44.0 KiB
+54 B
▁▃▃▃▃▃▃▃▃▃▃█
solid-start.full 51.4 KiB
+35 B
51.3 KiB
+33 B
152.5 KiB
-830 B
45.6 KiB
+70 B
▂▂▂▂▂▂▂▂▂▂▁█
vue-start.minimal 65.6 KiB
-91 B
65.5 KiB
-90 B
188.0 KiB
-1.2 KiB
58.5 KiB
+42 B
█████▃▃▃▃▃▃▁
vue-start.full 69.5 KiB
-59 B
69.4 KiB
-56 B
200.3 KiB
-1.2 KiB
61.7 KiB
-49 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.

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

7 package(s) bumped directly, 22 bumped as dependents.

🟩 Patch bumps

Package Version Reason
@tanstack/history 1.162.3 → 1.162.4 Changeset
@tanstack/react-router 1.170.35 → 1.170.36 Changeset
@tanstack/router-core 1.171.29 → 1.171.30 Changeset
@tanstack/router-devtools-core 1.168.1 → 1.168.2 Changeset
@tanstack/solid-router 1.170.33 → 1.170.34 Changeset
@tanstack/start-server-core 1.169.34 → 1.169.35 Changeset
@tanstack/vue-router 1.170.32 → 1.170.33 Changeset
@tanstack/react-router-devtools 1.167.1 → 1.167.2 Dependent
@tanstack/react-start 1.168.52 → 1.168.53 Dependent
@tanstack/react-start-client 1.168.33 → 1.168.34 Dependent
@tanstack/react-start-rsc 0.1.51 → 0.1.52 Dependent
@tanstack/react-start-server 1.167.40 → 1.167.41 Dependent
@tanstack/router-cli 1.167.35 → 1.167.36 Dependent
@tanstack/router-devtools 1.167.1 → 1.167.2 Dependent
@tanstack/router-generator 1.167.35 → 1.167.36 Dependent
@tanstack/router-plugin 1.168.37 → 1.168.38 Dependent
@tanstack/router-vite-plugin 1.167.37 → 1.167.38 Dependent
@tanstack/solid-router-devtools 1.167.1 → 1.167.2 Dependent
@tanstack/solid-start 1.168.50 → 1.168.51 Dependent
@tanstack/solid-start-client 1.168.32 → 1.168.33 Dependent
@tanstack/solid-start-server 1.167.39 → 1.167.40 Dependent
@tanstack/start-client-core 1.170.29 → 1.170.30 Dependent
@tanstack/start-plugin-core 1.171.42 → 1.171.43 Dependent
@tanstack/start-static-server-functions 1.167.34 → 1.167.35 Dependent
@tanstack/start-storage-context 1.167.31 → 1.167.32 Dependent
@tanstack/vue-router-devtools 1.167.1 → 1.167.2 Dependent
@tanstack/vue-start 1.168.49 → 1.168.50 Dependent
@tanstack/vue-start-client 1.167.35 → 1.167.36 Dependent
@tanstack/vue-start-server 1.167.39 → 1.167.40 Dependent

@pkg-pr-new

pkg-pr-new Bot commented Sep 12, 2026

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

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

@tanstack/eslint-plugin-router

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

@tanstack/eslint-plugin-start

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-rsc

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-fn-stubs

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

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

@tanstack/vue-start-client

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

@tanstack/vue-start-server

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

@tanstack/zod-adapter

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

commit: 65b6654

@codspeed

codspeed Bot commented Sep 12, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 18.29%

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

⚡ 3 improved benchmarks
✅ 177 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory mem client interrupted-navigations (vue) 282.9 KB 218.3 KB +29.6%
Memory mem client interrupted-navigations (react) 248.4 KB 216 KB +15.03%
Memory mem client interrupted-navigations (solid) 280.6 KB 252.8 KB +11.01%

Tip

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


Comparing schiller-manuel-search-middleware-compile-memory (65b6654) with schiller-manuel-remove-build-location-sharing (f5c4a4d)1

Open in CodSpeed

Footnotes

  1. No successful run was found on schiller-manuel-remove-build-location-sharing (e14d907) during the generation of this report, so 3adfe92 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

CodSpeed's memory instrument flagged the interrupted-navigations client
benchmark on the location-reuse changes (react +14.6%, vue +29.8%, solid
+10.8% peak memory). Under its worker flags (`--no-opt --predictable`,
single threaded) that benchmark's peak is set by a synchronous Maglev
compile that happens inside the measured run.

`getSearchMiddlewares` is compiled during the preparation calls and then
deoptimized on the first property load off a route's options. Before,
its only caller was the small `applySearchMiddleware`, which Maglev
compiled next and inlined it into, so the callee never tiered up again.
Since `build` calls it directly and does not inline it, it became hot
again during the measured run, and its recompilation (about four 32 KB
zone segments) set the peak.

Iterating `destRoutes` with a counted loop instead of `for...of` keeps the
function optimized once compiled: no deoptimization and no recompilation
in the measured window. Middleware order and live route-option reads are
unchanged. Hoisting the option reads alone did not help.

Local reproduction with a malloc interposer, CodSpeed's worker flags and
seven preparation calls, macOS arm64 / Node 24.8.0 (deterministic peaks):
- react: 248880 (base) -> 287760 (+15.6%) -> 248944 with this change
- vue:   256704 -> 292656 (+14.0%) -> 256640
- solid: 287408 -> 288240 (+0.3%)  -> 287632
These are local native allocations, not a new Linux CodSpeed CI run.
Bundle (react-router.minimal gzip): +8.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@schiller-manuel
schiller-manuel force-pushed the schiller-manuel-search-middleware-compile-memory branch from 6aec14d to 65b6654 Compare September 12, 2026 21:35
@schiller-manuel
schiller-manuel merged commit 9448caa into main Sep 13, 2026
25 of 26 checks passed
@schiller-manuel
schiller-manuel deleted the schiller-manuel-search-middleware-compile-memory branch September 13, 2026 11:55
Sheraff pushed a commit that referenced this pull request Sep 14, 2026
CodSpeed's memory instrument flagged the interrupted-navigations client
benchmark on the location-reuse changes (react +14.6%, vue +29.8%, solid
+10.8% peak memory). Under its worker flags (`--no-opt --predictable`,
single threaded) that benchmark's peak is set by a synchronous Maglev
compile that happens inside the measured run.

`getSearchMiddlewares` is compiled during the preparation calls and then
deoptimized on the first property load off a route's options. Before,
its only caller was the small `applySearchMiddleware`, which Maglev
compiled next and inlined it into, so the callee never tiered up again.
Since `build` calls it directly and does not inline it, it became hot
again during the measured run, and its recompilation (about four 32 KB
zone segments) set the peak.

Iterating `destRoutes` with a counted loop instead of `for...of` keeps the
function optimized once compiled: no deoptimization and no recompilation
in the measured window. Middleware order and live route-option reads are
unchanged. Hoisting the option reads alone did not help.

Local reproduction with a malloc interposer, CodSpeed's worker flags and
seven preparation calls, macOS arm64 / Node 24.8.0 (deterministic peaks):
- react: 248880 (base) -> 287760 (+15.6%) -> 248944 with this change
- vue:   256704 -> 292656 (+14.0%) -> 256640
- solid: 287408 -> 288240 (+0.3%)  -> 287632
These are local native allocations, not a new Linux CodSpeed CI run.
Bundle (react-router.minimal gzip): +8.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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.

2 participants