Skip to content

perf(router): keep the location cache client-only and slim Link SSR - #8390

Open
schiller-manuel wants to merge 2 commits into
schiller-manuel-search-middleware-compile-memoryfrom
schiller-manuel-ssr-link-performance
Open

perf(router): keep the location cache client-only and slim Link SSR#8390
schiller-manuel wants to merge 2 commits into
schiller-manuel-search-middleware-compile-memoryfrom
schiller-manuel-ssr-link-performance

Conversation

@schiller-manuel

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

Copy link
Copy Markdown
Collaborator

Stacked on #8385.

🎯 Changes

Why

  • The per-options buildLocation cache (staticLocations, from the location-reuse layers) only ever hits on the client: server renders never repeat an options object. Yet every server build still created the WeakMap, looked it up per buildLocation, and Solid/Vue server Links (which pass a fresh { _fromLocation, ...options } per render) stored an entry per render.
  • Rendering a React Link on the server copied its props four times: Link split off _asChild with an object rest, useLinkProps split the router options off with another object rest, the result was assembled with spreads, and Link copied once more to drop type/disabled. Profiling showed the object rest alone was ~85% of the hook's self time: V8 checks every key of an object rest against the whole 35-entry exclusion list (~470 ns per call vs ~95 ns for a key-set copy).

What changed

  1. router-core: staticLocations is client-only. The field is now WeakMap | undefined; its creation in update() and setRoutes(), the read at the top of buildLocation, and the write at its end are all guarded by !(isServer ?? this.isServer). isServer from @tanstack/router-core/isServer is a per-bundle constant (false in browser builds, true in server builds), so bundlers fold the guards: server bundles contain no cache at all, client bundles keep the cache logic unchanged.
  2. react-router: slimmer Link server render, no extra client copies. useLinkProps is now a thin wrapper over useLinkPropsFor(options, ref, host?). Link calls it with its host ('a' or the createLink component), so the hook omits disabled for the anchor host and type for both hosts, and Link passes the result straight to createElement (no copies). The server branch is a separate getServerLinkProps referenced only inside if (isServer ?? router.isServer), so it and its LINK_OPTION_KEYS set are dropped from client bundles. It reads the few options it needs directly, splits element props with the key set, fills the props object in place with the same precedence and attribute order as the previous spreads, and no longer calls useForwardedRef (that hook now sits after the server return; a server render never re-renders and the returned ref is the forwarded ref). The client path keeps its original rest destructure (plus _asChild/type extraction and host !== 'a' around disabled).
  3. router-core encodePathLikeUrl tests with one merged character class /[\s\u0080-\uFFFF]/ instead of the alternation /\s|[^\u0000-\u007F]/. Verified identical test() results for every UTF-16 code unit (0x0000–0xFFFF) plus surrogate-pair samples; the replacement regex is unchanged. About 10% cheaper.
  4. react-router: one state-prop precedence rule for every Link (second commit, prompted by the review question on the blockedLink branch). activeProps/inactiveProps used to override ref and the event handlers on every link except a blocked one (destination with a disallowed scheme), where the forwarded ref and the router's handlers won. That exception was a leftover of the hardening change's original "router wins" ordering, kept alive by a test when state props were later allowed to override element props. It bought no safety (href, disabled, target are assigned after the state props in both orders, which is what keeps a blocked destination inert), it made the same inactiveProps attach their ref/onClick on one inactive link and silently drop them on another, and it differed from Solid's Link. Blocked links now follow the same rule as every other link; the server loses its if/else, the client loses two conditional spreads and the blockedLink flag. The blocked-link test pins the uniform rule together with the routing attributes it already checked. Noted in the changeset.

Measurements (local, macOS arm64 / Node 24.8.0 — not CI)

Paired-runner and Start SSR numbers for the first commit are the stack author's local measurements:

  • Link SSR paired runner (@benchmarks/react-link-performance:test:perf:stable --mode ssr, ABBA blocks, 4 fresh processes per case) against a dist snapshot of the previous stack top: all 13 cases "faster", CPU −19.5% .. −40.3% (shared-params −39.4%, unique-params −40.3%, param-updaters −36.4%, location-updaters −23.8%, relative −23.6%, middleware −19.5%, numeric-params −37.5%, optional-params −38.0%, splats −31.2%, encoding −21.1%, masks −29.4%, rewrites −20.7%, active −32.9%).
  • Link client paired runner (--mode client): all 13 cases "faster", CPU −4.6% .. −9.2% (from the removed Link copies).
  • Start SSR request loop (benchmarks/ssr react baseline app, interleaved builds, 4+4 slots, rme < 1.2%): 3.120 → 2.890 ms per 10-request loop (−7.4%), 320.5 → 346.1 hz (+8.0%).

Second commit (uniform precedence), paired runner with 3 repeats against the first commit:

  • Link client: shared-params CPU −11.6% [−15.1, −7.9], encoding −13.2% [−20.2, −5.7] (two fewer spreads per render).
  • Link SSR: within noise.

Bundle sizes measured in this PR's worktree with the official runner (benchmark:bundle-size:run, cache-free rebuilds on both sides):

  • react-router.minimal gzip: 86012 → 86019 (+7); raw −31, brotli −55. Per-hunk attribution: the router-core hunks alone are −12 gzip (the guards fold away and the regex is shorter); the link.tsx changes cost the remainder (the _asChild/type rest exclusions, the type re-add for the public hook and the host !== 'a' guards are not fully paid back by the removed Link copies). The second commit is −2 gzip on its own.
  • Link SSR benchmark bundle (benchmarks/client-nav/link-performance/dist/ssr/app.js): 197832 → 189850 bytes, because the client-only Link code no longer ships to the server.

Bundle-content assertions (verified after TSR_LINK_PERF=1 pnpm nx run-many --target=build:ssr,build:client --projects=@benchmarks/react-link-performance)

  • dist/ssr/app.js: 0 staticLocations references (was 4), contains getServerLinkProps and the LINK_OPTION_KEYS set, 0 useForwardedRef references.
  • dist/client/app.js: 4 staticLocations references (unchanged), 0 getServerLinkProps, 0 "preloadIntentProximity" string literal (the key set is gone; the two remaining preloadIntentProximity occurrences are the client rest destructure, as before).

Tests run

  • @tanstack/router-core:test:unit — 133 files, 3303 passed | 4 expected fail
  • @tanstack/react-router:test:unit — 91 files, 1174 passed | 1 skipped (re-run after the second commit)
  • @tanstack/solid-router:test:unit — 942 passed | 1 skipped (+ 51 in the second project)
  • @tanstack/vue-router:test:unit — 968 passed | 1 skipped
  • test:types + test:eslint for @tanstack/router-core and @tanstack/react-router — types clean, eslint 0 errors (only pre-existing warnings, none in link.tsx, none on changed lines of router.ts/utils.ts)
  • git diff --check and prettier on all changed files — clean

✅ 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

  • Bug Fixes
    • Improved link behavior during server rendering by avoiding unnecessary location caching and prop handling.
    • Standardized active and inactive link properties across regular, custom, and blocked links.
    • Ensured router-controlled attributes such as href, disabled, and target are applied consistently.
    • Prevented unsupported attributes from being forwarded to standard anchor elements.

The per-options location cache from the location-reuse changes only ever
hits on the client: server renders never repeat an options object, yet
every server build still created the WeakMap, looked it up per build, and
Solid/Vue server Links stored an entry per render. All three sites are now
guarded by `!(isServer ?? this.isServer)`. `isServer` is a per-bundle
constant, so server bundles contain no cache at all (the Link SSR bundle
has zero references); client bundles keep it unchanged.

Rendering a React Link on the server copied its props four times: `Link`
split off `_asChild`, `useLinkProps` split the router options off with an
object rest, the result was assembled with spreads, and `Link` copied once
more to drop `type` and `disabled`. Profiling showed the object rest alone
was 85% of the hook's self time: V8 checks every key against the whole
35-entry exclusion list (about 470 ns per call versus 95 ns for a key-set
copy).

`useLinkProps` is now a wrapper over `useLinkPropsFor(options, ref, host)`.
`Link` passes its host (`'a'` or the `createLink` component), so the hook
omits `disabled` for anchors and `type` for both, and `Link` passes the
result straight to `createElement`. The server branch is a separate
`getServerLinkProps` referenced only inside the `isServer` check, so it and
its key set are dropped from client bundles; it reads the few options it
needs, splits element props with the key set, fills the props object in
place with the same precedence and attribute order as before, and no
longer runs `useForwardedRef` (moved after the server return). The client
keeps its rest destructure; the host handling costs a few bytes there,
mostly paid back by the folded router-core guards (numbers below).

`encodePathLikeUrl` tests with one merged character class
(`/[\s\u0080-\uFFFF]/`), which matches exactly the same code units as the
alternation it replaces and is about 10% cheaper.

Measurements (macOS arm64, Node 24.8.0, local):
- Link SSR paired runner (ABBA, 4 fresh processes per case) against the
  previous stack top: all 13 cases faster, CPU -19.5% to -40.3%.
- Link client paired runner: all 13 cases faster, CPU -4.6% to -9.2%.
- Start SSR request loop (benchmarks/ssr, react), interleaved builds:
  3.120 -> 2.890 ms per loop (-7.4%), 320.5 -> 346.1 hz.
- Client bundle react-router.minimal gzip: 86012 -> 86021 (+9; raw +7,
  brotli -22): the router-core guards fold to -12, the Link host handling
  costs +19. The Link SSR bundle shrinks from 197832 to 189989 bytes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Sep 12, 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: ca7ec506-c2ab-4e6b-8c75-62156fe53772

📥 Commits

Reviewing files that changed from the base of the PR and between 98ae4cf and 949e507.

📒 Files selected for processing (3)
  • .changeset/brisk-links-serve.md
  • packages/react-router/src/link.tsx
  • packages/react-router/tests/link-state-props.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/brisk-links-serve.md

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


📝 Walkthrough

Walkthrough

The router now avoids location-cache allocation on the server. React Links use host-aware server and client prop generation with consistent state-prop precedence. Blocked custom links and related tests now cover state refs and handlers.

Changes

Server rendering behavior

Layer / File(s) Summary
Client-only location cache
packages/router-core/src/router.ts
Location-cache allocation, reset, lookup, and writes now occur only during client execution.
Host-aware Link props and blocked-link state
packages/react-router/src/link.tsx, packages/react-router/tests/link-state-props.test.tsx
Link prop generation now uses the host element, filters server props through LINK_OPTION_KEYS, omits anchor disabled, and applies state props consistently. Tests cover blocked custom-link refs, targets, and handlers.
URL guard and release metadata
packages/router-core/src/utils.ts, .changeset/brisk-links-serve.md
The URL guard keeps the same matching behavior, and patch releases document the Link and cache changes.

Priority: ➖ Normal

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

Change: Refactor

Suggested reviewers: sheraff

Merge Risk: ⚪ Minimal · up to 949e5

No merge-blocking issue was identified in the client-only cache or Link prop-generation changes.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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 5 functions across 4 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.
Title check ✅ Passed The title clearly summarizes the primary changes: keeping the router location cache client-only and reducing React Link server-rendering overhead.
Description check ✅ Passed The description is complete and directly related to the pull request. It includes the motivation, detailed changes, measurements, verification results, completed checklist items, and release impact wi…
✨ 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-ssr-link-performance

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 Sep 12, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 949e507

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ⏳ In Progress ... View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2m 14s View ↗

☁️ Nx Cloud last updated this comment at 2026-09-12 10:27:19 UTC

@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@8390

@tanstack/eslint-plugin-router

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

@tanstack/eslint-plugin-start

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-rsc

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-fn-stubs

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

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

@tanstack/vue-start-client

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

@tanstack/vue-start-server

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

@tanstack/zod-adapter

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

commit: 949e507

@github-actions

github-actions Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: 5a19c749d9c6
  • Measured at: 2026-09-12T10:26:19.522Z
  • Baseline source: history:f021f6d1c6dc
  • 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
+26 B
83.9 KiB
+20 B
261.6 KiB
-726 B
73.2 KiB
-8 B
███████▁▁▁▁▂
react-router.full 87.5 KiB
+26 B
87.4 KiB
+28 B
273.3 KiB
-699 B
76.3 KiB
+36 B
███████▁▁▁▁▃
solid-router.minimal 33.4 KiB
-32 B
33.3 KiB
-30 B
95.9 KiB
-833 B
30.3 KiB
-1 B
▆▆▆████████▁
solid-router.full 38.3 KiB
-5 B
38.2 KiB
-2 B
110.6 KiB
-832 B
34.5 KiB
+53 B
▂▂▁████████▁
vue-router.minimal 49.5 KiB
-96 B
49.4 KiB
-94 B
137.1 KiB
-1.2 KiB
44.8 KiB
-115 B
███████▃▃▃▃▁
vue-router.full 55.1 KiB
-92 B
55.0 KiB
-91 B
155.3 KiB
-1.2 KiB
49.7 KiB
-64 B
███████▃▃▃▃▁
react-start.minimal 96.9 KiB
+24 B
96.8 KiB
+27 B
303.9 KiB
-719 B
84.0 KiB
-67 B
███████▁▁▁▁▂
react-start.query-integration 104.3 KiB
+18 B
104.1 KiB
+20 B
330.4 KiB
-727 B
90.3 KiB
-4 B
███████▁▁▁▁▂
react-start.deferred-hydration 97.6 KiB
+34 B
96.8 KiB
+34 B
305.2 KiB
-718 B
84.7 KiB
-42 B
███████▁▁▁▁▃
react-start.full 100.1 KiB
+35 B
99.9 KiB
+33 B
313.6 KiB
-704 B
86.8 KiB
+111 B
███████▁▁▁▁▃
react-start.rsbuild.minimal 100.2 KiB
+64 B
100.0 KiB
+64 B
314.2 KiB
-622 B
86.6 KiB
+130 B
███████▁▁▁▁▅
react-start.rsbuild.minimal-iife 100.6 KiB
+71 B
100.4 KiB
+71 B
315.2 KiB
-605 B
86.9 KiB
+48 B
███████▁▁▁▁▅
react-start.rsbuild.full 103.5 KiB
+66 B
103.3 KiB
+66 B
324.3 KiB
-606 B
89.2 KiB
-5 B
███████▁▁▁▁▄
solid-start.minimal 46.4 KiB
+30 B
46.3 KiB
+31 B
137.1 KiB
-833 B
41.2 KiB
+46 B
▂▂▃▁▁▁▁▁▁▁▁█
solid-start.deferred-hydration 49.4 KiB
+18 B
46.3 KiB
+17 B
144.4 KiB
-831 B
44.0 KiB
+17 B
▆▆▁▃▃▃▃▃▃▃▃█
solid-start.full 51.4 KiB
+23 B
51.3 KiB
+23 B
152.5 KiB
-831 B
45.6 KiB
+56 B
▂▂▂▁▁▁▁▁▁▁▁█
vue-start.minimal 65.6 KiB
-99 B
65.5 KiB
-99 B
187.9 KiB
-1.2 KiB
58.5 KiB
-115 B
███████▃▃▃▃▁
vue-start.full 69.5 KiB
-63 B
69.4 KiB
-63 B
200.3 KiB
-1.2 KiB
61.9 KiB
+80 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.

@codspeed-hq

codspeed-hq Bot commented Sep 12, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 5.97%

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

⚡ 1 improved benchmark
✅ 179 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation ssr request loop (react) 171.2 ms 161.5 ms +5.97%

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-ssr-link-performance (98ae4cf) with schiller-manuel-search-middleware-compile-memory (6aec14d)

Open in CodSpeed

@schiller-manuel
schiller-manuel added this pull request to stack #8346 September 12, 2026 09:22
Comment thread packages/react-router/src/link.tsx Outdated
} else {
props.ref = forwardedRef
Object.assign(props, resolvedStateProps)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does this distinction matter?

const STATIC_DISABLED_PROPS = { role: 'link', 'aria-disabled': true }
const STATIC_ACTIVE_PROPS = { 'data-status': 'active', 'aria-current': 'page' }
// Options consumed by the router and never forwarded to the element.
const LINK_OPTION_KEYS = /* @__PURE__ */ new Set([

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm assuming this was verified to only exist in the server build?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. not leaking into the client since only referenced in the server branch from getServerLinkProps

Active and inactive state props overrode `ref` and the event handlers on
every link except a blocked one (a destination with a disallowed scheme),
where the forwarded ref and the router's handlers won. The exception dates
from the hardening change, whose test observed the then-general "router
wins" rule on a blocked link; when state props were later allowed to
override element props, blocked links were carved out to keep that test
green rather than by design. It bought no safety: `href`, `disabled` and
`target` are assigned after the state props in both orders, which is what
actually keeps a blocked destination inert. It did make the same
`inactiveProps` attach their `ref` and `onClick` on one inactive link and
silently drop them on another, and it differed from Solid's Link, which
applies one rule.

Blocked links now follow the same precedence as every other link: state
props override element props, `ref` and handlers; the routing attributes
always win. The server branch loses its if/else, the client loses the two
conditional spreads and the `blockedLink` flag. The blocked-link test now
pins the uniform rule together with the routing attributes it already
checked.

Measurements (macOS arm64, Node 24.8.0, local, against the previous commit):
- react-router.minimal gzip 86021 -> 86019 (-2), raw -38, brotli -33.
- Link client paired runner (3 repeats): shared-params CPU -11.6%
  [-15.1, -7.9], encoding -13.2% [-20.2, -5.7].
- Link SSR paired runner: within noise. SSR bundle 189989 -> 189850 bytes.

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