Skip to content

perf(react-router): avoid redundant Link state prop allocations - #8311

Merged
schiller-manuel merged 1 commit into
mainfrom
optimize-link-2
Sep 9, 2026
Merged

schiller-manuel merged 1 commit into
mainfrom
optimize-link-2

Conversation

@schiller-manuel

@schiller-manuel schiller-manuel commented Sep 9, 2026 •

Copy link
Copy Markdown
Collaborator

Links resolve one state-prop bag instead of separate active and inactive bags. Class names do not need temporary arrays. Unchanged styles retain their original references. Callback behavior and prop precedence remain unchanged.

React Link rendering benchmark, measured separately for this commit and its parent using exact production builds:

  • Workload: benchmarks/client-nav/scenarios/links/react/speed.bench.ts (200 persistent Links, eight navigations per measured batch).
  • Apple M4, Node 24.20.0, Vitest 4.1.4, NODE_ENV=production.
  • 8 fresh parent processes and 8 fresh processes for this commit, each with warmupIterations=50 and time=10000 ms; counterbalanced run order.
  • Parent mean times (ms): 3.9412, 3.8998, 3.8950, 4.2674, 4.0423, 3.8793, 3.9043, 4.1120.
  • This commit mean times (ms): 3.8767, 3.8900, 4.5507, 4.0116, 3.8297, 3.8582, 3.8879, 4.0157.
  • Median run means: 3.9228 -> 3.8889 ms.
  • Incremental effect: 0.86% less time; throughput change +0.87%.
  • Largest within-run RME for this revision: 3.08%.
  • The small difference is not a reliable speedup claim; it is close to process-to-process variation.

Incremental minimal/full React gzip impact: -12/-7 bytes. Benchmark sources and commit implementation trees are unchanged.

🎯 Changes

✅ 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 React Link styling so active and inactive state properties resolve correctly during navigation.
    • Base class names and styles are now preserved while state-specific styling is merged as expected.
    • Returning to the original route correctly restores the link’s default appearance.
  • Tests

    • Added coverage for style and class-name behavior across active and inactive navigation states.

Links resolve one state-prop bag instead of separate active and inactive bags. Class names do not need temporary arrays. Unchanged styles retain their original references. Callback behavior and prop precedence remain unchanged.

React Link rendering benchmark, measured separately for this commit
and its parent using exact production builds:
- Workload: benchmarks/client-nav/scenarios/links/react/speed.bench.ts
  (200 persistent Links, eight navigations per measured batch).
- Apple M4, Node 24.20.0, Vitest 4.1.4, NODE_ENV=production.
- 8 fresh parent processes and 8 fresh processes for this commit,
  each with warmupIterations=50 and time=10000 ms;
  counterbalanced run order.
- Parent mean times (ms): 3.9412, 3.8998, 3.8950, 4.2674, 4.0423, 3.8793, 3.9043, 4.1120.
- This commit mean times (ms): 3.8767, 3.8900, 4.5507, 4.0116, 3.8297, 3.8582, 3.8879, 4.0157.
- Median run means: 3.9228 -> 3.8889 ms.
- Incremental effect: 0.86% less time; throughput change +0.87%.
- Largest within-run RME for this revision: 3.08%.
- The small difference is not a reliable speedup claim; it is close to
  process-to-process variation.

Incremental minimal/full React gzip impact: -12/-7 bytes.
Benchmark sources and commit implementation trees are unchanged.

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

coderabbitai Bot commented Sep 9, 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: 6356e3ae-989f-4e95-8d60-fc68ac13b16b

📥 Commits

Reviewing files that changed from the base of the PR and between 539e598 and 238fe17.

📒 Files selected for processing (3)
  • .changeset/honest-nails-burn.md
  • packages/react-router/src/link.tsx
  • packages/react-router/tests/link.test.tsx

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


📝 Walkthrough

Walkthrough

useLinkProps now selects one active or inactive props object before merging class names and styles. A new test verifies style reuse, active overrides, and restoration after navigation. A patch changeset documents the update.

Changes

Link state prop resolution

Layer / File(s) Summary
Resolve and validate link state props
packages/react-router/src/link.tsx, packages/react-router/tests/link.test.tsx, .changeset/honest-nails-burn.md
useLinkProps selects one state-specific props object, merges its class and style values with the base values, and spreads only the selected props onto the anchor. Tests verify inactive, active, and restored link states. The changeset records the patch release.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 238fe

Link rendering now avoids redundant state-prop allocations while preserving active and inactive class/style behavior. No current merge-blocking risk remains.

Suggested reviewers: sheraff

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 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 optimization: avoiding redundant React Router Link state-prop allocations.
Description check ✅ Passed The description explains the implementation, behavior guarantees, benchmark results, and release impact. The required sections are present, but all checklist and release-impact boxes remain unchecked.
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 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 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 optimize-link-2

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 9, 2026 •

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 238fe17

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

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

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

8 package(s) bumped directly, 18 bumped as dependents.

🟩 Patch bumps

Package Version Reason
@tanstack/history 1.162.2 → 1.162.3 Changeset
@tanstack/nitro-v2-vite-plugin 1.155.1 → 1.155.2 Changeset
@tanstack/react-router 1.170.33 → 1.170.34 Changeset
@tanstack/router-core 1.171.28 → 1.171.29 Changeset
@tanstack/router-generator 1.167.34 → 1.167.35 Changeset
@tanstack/router-plugin 1.168.36 → 1.168.37 Changeset
@tanstack/router-utils 1.162.2 → 1.162.3 Changeset
@tanstack/start-plugin-core 1.171.40 → 1.171.41 Changeset
@tanstack/react-start 1.168.50 → 1.168.51 Dependent
@tanstack/react-start-client 1.168.31 → 1.168.32 Dependent
@tanstack/react-start-rsc 0.1.49 → 0.1.50 Dependent
@tanstack/react-start-server 1.167.38 → 1.167.39 Dependent
@tanstack/router-cli 1.167.34 → 1.167.35 Dependent
@tanstack/router-vite-plugin 1.167.36 → 1.167.37 Dependent
@tanstack/solid-router 1.170.31 → 1.170.32 Dependent
@tanstack/solid-start 1.168.48 → 1.168.49 Dependent
@tanstack/solid-start-client 1.168.30 → 1.168.31 Dependent
@tanstack/solid-start-server 1.167.37 → 1.167.38 Dependent
@tanstack/start-client-core 1.170.28 → 1.170.29 Dependent
@tanstack/start-server-core 1.169.32 → 1.169.33 Dependent
@tanstack/start-static-server-functions 1.167.33 → 1.167.34 Dependent
@tanstack/start-storage-context 1.167.30 → 1.167.31 Dependent
@tanstack/vue-router 1.170.30 → 1.170.31 Dependent
@tanstack/vue-start 1.168.47 → 1.168.48 Dependent
@tanstack/vue-start-client 1.167.33 → 1.167.34 Dependent
@tanstack/vue-start-server 1.167.37 → 1.167.38 Dependent

@pkg-pr-new

pkg-pr-new Bot commented Sep 9, 2026

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

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

@tanstack/eslint-plugin-router

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

@tanstack/eslint-plugin-start

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-rsc

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-fn-stubs

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

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

@tanstack/vue-start-client

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

@tanstack/vue-start-server

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

@tanstack/zod-adapter

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

commit: 238fe17

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: 50a6e6942135
  • Measured at: 2026-09-09T19:57:23.225Z
  • Baseline source: history:539e5985cf28
  • 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.9 KiB
-19 B
83.8 KiB
-17 B
262.5 KiB
-54 B
73.0 KiB
+17 B
▁▂▂▂▃▃▃▇███▇
react-router.full 87.4 KiB
-8 B
87.2 KiB
-7 B
274.1 KiB
-54 B
76.1 KiB
+23 B
▁▂▃▃▃▃▃▆███▇
react-start.minimal 96.8 KiB
-6 B
96.6 KiB
-8 B
304.7 KiB
-53 B
83.9 KiB
+61 B
▁▂▃▃▃▃▃▇███▇
react-start.query-integration 104.1 KiB
-14 B
104.0 KiB
-11 B
331.2 KiB
-49 B
90.3 KiB
-4 B
▁▂▃▃▃▃▃▇███▇
react-start.deferred-hydration 97.5 KiB
-6 B
96.7 KiB
-2 B
306.1 KiB
-53 B
84.6 KiB
+7 B
▁▂▃▃▃▃▃▇███▇
react-start.full 100.0 KiB
-21 B
99.8 KiB
-20 B
314.4 KiB
-54 B
86.6 KiB
-53 B
▁▂▃▃▃▃▃▇███▆
react-start.rsbuild.minimal 100.1 KiB
-7 B
99.9 KiB
-7 B
315.0 KiB
-52 B
86.3 KiB
-91 B
▁▂▃▃▂▂▂▇███▇
react-start.rsbuild.minimal-iife 100.5 KiB
-10 B
100.3 KiB
-10 B
315.9 KiB
-52 B
86.7 KiB
+60 B
▁▂▃▃▂▂▂▇███▇
react-start.rsbuild.full 103.4 KiB
-10 B
103.2 KiB
-10 B
325.1 KiB
-52 B
89.0 KiB
-67 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

codspeed Bot commented Sep 9, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 180 untouched benchmarks


Comparing optimize-link-2 (238fe17) with main (539e598)

Open in CodSpeed

@schiller-manuel
schiller-manuel merged commit 9aec5a7 into main Sep 9, 2026
26 checks passed
@schiller-manuel
schiller-manuel deleted the optimize-link-2 branch September 9, 2026 20:22
@github-actions github-actions Bot mentioned this pull request Sep 9, 2026
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