Skip to content

fix(react-router): avoid rewriting unchanged head assets - #8257

Merged
schiller-manuel merged 1 commit into
mainfrom
schiller-manuel-head-asset-stability
Sep 5, 2026
Merged

fix(react-router): avoid rewriting unchanged head assets#8257
schiller-manuel merged 1 commit into
mainfrom
schiller-manuel-head-asset-stability

Conversation

@schiller-manuel

@schiller-manuelschiller-manuel commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

🎯 Changes

React 19 rewrites unchanged head content when dangerouslySetInnerHTML gets a new object. This replaces the stylesheet during navigation and can fail under Trusted Types.

  • Memoize HTML payloads for styles and data scripts.
  • Extend the existing inline-CSS e2e fixture with native DOM/CSSOM assertions and enforced-CSP navigation coverage for Vite and Rsbuild. No setter patches or permissive policies.
  • Use native script lookup and URL resolution to offset the added code. Keep server guards directly in conditionals for dead-code elimination.

This prevents redundant writes after hydration, but does not add general Trusted Types support.

All 18 bundle scenarios match or improve the primary gzip metric against origin/main: eight shrink by 19-31 bytes. Raw output shrinks by 47 bytes in affected scenarios. Brotli increases by 57-175 bytes in five scenarios.

Fixes#8250

✅ 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
    • Prevented unnecessary rewrites of unchanged inline styles and scripts during React re-renders.
    • Improved client navigation compatibility with Trusted Types policies, avoiding related security errors.
    • Preserved inline stylesheets across route changes, reducing unnecessary CSS processing.
    • Prevented duplicate external and inline scripts from being added during navigation.
    • Ensured asset content and DOM elements remain stable when rendered content has not changed.

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

coderabbitaiBot commented Sep 5, 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: 86ae3a32-a58b-4459-89e4-41457e76fc9c

📥 Commits

Reviewing files that changed from the base of the PR and between 0497cae and 2e38f88.

📒 Files selected for processing (5)
  • .changeset/spotty-poems-smell.md
  • e2e/react-start/css-inline/src/routes/app/dashboard/index.tsx
  • e2e/react-start/css-inline/tests/css-inline.spec.ts
  • packages/react-router/src/Asset.tsx
  • packages/react-router/tests/Asset.test.tsx

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


📝 Walkthrough

Walkthrough

Asset now memoizes dangerouslySetInnerHTML values and improves script deduplication. Unit and end-to-end tests cover rerenders, navigation, stylesheet stability, script reuse, and Trusted Types enforcement.

Changes

Asset rendering and navigation

Layer / File(s)Summary
Stable HTML and script deduplication
packages/react-router/src/Asset.tsx
Memoized HTML objects prevent unchanged style and script content from being rewritten. Script detection now scans document.scripts and separates external and inline scripts.
Asset regression tests
packages/react-router/tests/Asset.test.tsx
Tests cover stable DOM nodes, script attributes, cleanup, external script reuse, and inline-script deduplication.
Navigation and release validation
e2e/react-start/css-inline/src/routes/app/dashboard/index.tsx, e2e/react-start/css-inline/tests/css-inline.spec.ts, .changeset/spotty-poems-smell.md
End-to-end tests verify stylesheet identity, zero mutations, and navigation without Trusted Types violations or browser errors. The changeset records a patch release.
Estimated code review effort: 3 (Moderate)~25 minutes

Merge Risk:⚪ Minimal · up to 2e38f

This change stabilizes unchanged inline style and script content across navigation, preventing redundant browser writes and Trusted Types failures. The included regression coverage supports merge readiness.

Sequence Diagram(s)

sequenceDiagram
participant React19
participant Asset
participant DOM
participant Browser
React19->>Asset: rerender route assets
Asset->>React19: provide memoized HTML object
React19->>DOM: retain unchanged style or script content
Asset->>Browser: inspect existing scripts
Browser-->>Asset: return matching script elements
Asset->>DOM: reuse or inject script
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 4 files. (1 skipped: 1 …Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and concisely identifies the primary change: preventing rewrites of unchanged React Router head assets.
Description check✅ PassedThe description follows the required template, explains the change and motivation, documents testing and release impact, and includes the required changeset information.
Linked Issues check✅ PassedThe implementation addresses issue #8250 by memoizing HTML payloads for styles and data scripts. The added unit and end-to-end tests verify unchanged assets are not rewritten and that enforced Trusted…
Out of Scope Changes check✅ PassedAll changes are related to issue #8250 and the stated objectives. The source fix, regression tests, end-to-end coverage, and changeset are in scope.
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 3 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 schiller-manuel-head-asset-stability

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

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

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

🟩 Patch bumps

PackageVersionReason
@tanstack/nitro-v2-vite-plugin1.155.0 → 1.155.1Changeset
@tanstack/react-router1.170.32 → 1.170.33Changeset
@tanstack/router-core1.171.27 → 1.171.28Changeset
@tanstack/solid-router1.170.30 → 1.170.31Changeset
@tanstack/start-plugin-core1.171.39 → 1.171.40Changeset
@tanstack/vue-router1.170.29 → 1.170.30Changeset
@tanstack/react-start1.168.49 → 1.168.50Dependent
@tanstack/react-start-client1.168.30 → 1.168.31Dependent
@tanstack/react-start-rsc0.1.48 → 0.1.49Dependent
@tanstack/react-start-server1.167.37 → 1.167.38Dependent
@tanstack/router-cli1.167.33 → 1.167.34Dependent
@tanstack/router-generator1.167.33 → 1.167.34Dependent
@tanstack/router-plugin1.168.35 → 1.168.36Dependent
@tanstack/router-vite-plugin1.167.35 → 1.167.36Dependent
@tanstack/solid-start1.168.47 → 1.168.48Dependent
@tanstack/solid-start-client1.168.29 → 1.168.30Dependent
@tanstack/solid-start-server1.167.36 → 1.167.37Dependent
@tanstack/start-client-core1.170.27 → 1.170.28Dependent
@tanstack/start-server-core1.169.31 → 1.169.32Dependent
@tanstack/start-static-server-functions1.167.32 → 1.167.33Dependent
@tanstack/start-storage-context1.167.29 → 1.167.30Dependent
@tanstack/vue-start1.168.46 → 1.168.47Dependent
@tanstack/vue-start-client1.167.32 → 1.167.33Dependent
@tanstack/vue-start-server1.167.36 → 1.167.37Dependent

@nx-cloud

nx-cloudBot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 2e38f88

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded15m 9sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded2m 3sView ↗

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

@github-actions

Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: 51406bc3813b
  • Measured at: 2026-09-05T21:53:32.298Z
  • Baseline source: history:0497caeef3ff
  • Dashboard: bundle-size history

The following scenarios have bundle-size changes compared with the baseline:

ScenarioCurrent (gzip)Initial (gzip)RawBrotliTrend
react-router.full87.2 KiB
-31 B
87.1 KiB
-31 B
273.7 KiB
-47 B
76.0 KiB
-36 B
██▂▅▅▆▆▆▆▆▆▁
react-start.minimal96.7 KiB
-19 B
96.5 KiB
-18 B
304.3 KiB
-47 B
83.8 KiB
-13 B
▇█▁▅▇▆▅▇▇▇▅▂
react-start.query-integration104.0 KiB
-28 B
103.9 KiB
-31 B
330.8 KiB
-47 B
90.2 KiB
+70 B
▆▇▁▄▅▅▅▅▅▅█▄
react-start.deferred-hydration97.4 KiB
-25 B
96.5 KiB
-20 B
305.6 KiB
-47 B
84.5 KiB
+67 B
▇█▁▃▆▆▅▆▆▆█▃
react-start.full99.8 KiB
-23 B
99.7 KiB
-24 B
314.0 KiB
-47 B
86.4 KiB
-28 B
▇█▁▅▆▇▆▆▆▆█▄
react-start.rsbuild.minimal100.0 KiB
-29 B
99.8 KiB
-29 B
314.6 KiB
-47 B
86.3 KiB
+175 B
██▁▄▃▃▂▃▃▃█▃
react-start.rsbuild.minimal-iife100.4 KiB
-24 B
100.2 KiB
-24 B
315.5 KiB
-47 B
86.7 KiB
+89 B
██▁▃▃▃▂▃▃▃▇▃
react-start.rsbuild.full103.3 KiB
-23 B
103.1 KiB
-23 B
324.7 KiB
-47 B
89.0 KiB
+57 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-newBot commented Sep 5, 2026

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

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

@tanstack/eslint-plugin-router

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

@tanstack/eslint-plugin-start

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-rsc

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-fn-stubs

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

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

@tanstack/vue-start-client

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

@tanstack/vue-start-server

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

@tanstack/zod-adapter

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

commit: 2e38f88

@schiller-manuel
schiller-manuel merged commit cf166d1 into mainSep 5, 2026
25 checks passed
@schiller-manuel
schiller-manuel deleted the schiller-manuel-head-asset-stability branch September 5, 2026 22:21
@codspeed-hq

Copy link
Copy Markdown

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

⚡ 9 improved benchmarks
❌ 4 regressed benchmarks
✅ 167 untouched benchmarks
⏩ 48 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

ModeBenchmarkBASEHEADEfficiency
Memorymem server aborted-requests (vue)1,014.4 KB1,110.8 KB-8.68%
Memorymem server error-paths redirect (react)316.6 KB334.2 KB-5.26%
Memorymem server server-fn-churn (vue)345.8 KB364.7 KB-5.18%
Memorymem server server-fn-churn (react)409.2 KB423.2 KB-3.31%
Memorymem server error-paths redirect (vue)454.2 KB410.9 KB+10.53%
Memorymem server error-paths not-found (react)450.3 KB408.5 KB+10.21%
Memorymem client unique-location-churn (solid)409.6 KB385.1 KB+6.36%
Memorymem client mount-unmount (solid)511 KB482.3 KB+5.94%
Memorymem client interrupted-navigations (vue)356.7 KB340.1 KB+4.86%
Memorymem server error-paths not-found (vue)503.4 KB482.9 KB+4.24%
Memorymem server peak-large-page (vue)1 MB1 MB+3.96%
Memorymem server error-paths not-found (solid)772.7 KB743.4 KB+3.95%
Memorymem client navigation-churn (vue)1.6 MB1.5 MB+3.17%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing schiller-manuel-head-asset-stability (2e38f88) with main (0497cae)

Open in CodSpeed

Footnotes

  1. 48 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Sign up for freeto 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.

InlineCssStyle re-assigns innerHTML on every render — a full stylesheet re-parse per navigation, and a dead page under Trusted Types

1 participant

@schiller-manuel