Skip to content

fix: dedupe repeated css assets in the start manifest - #7030

Merged
schiller-manuel merged 1 commit into
mainfrom
fix-dedupe-css
Mar 24, 2026
Merged

fix: dedupe repeated css assets in the start manifest#7030
schiller-manuel merged 1 commit into
mainfrom
fix-dedupe-css

Conversation

@schiller-manuel

@schiller-manuelschiller-manuel commented Mar 24, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • Bug Fixes
    • CSS assets in the Start manifest are now properly deduplicated. Shared stylesheets no longer appear multiple times within individual route entries or across active parent-child route chains, reducing manifest size.

@coderabbitai

coderabbitaiBot commented Mar 24, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR introduces CSS asset deduplication to the Start manifest plugin by refactoring the preload deduplication logic to handle both preloads and stylesheets, preventing duplicate CSS from appearing in route entries or across parent-child route chains.

Changes

Cohort / File(s)Summary
Release Documentation
.changeset/fair-rivers-drum.md
Marks @tanstack/start-plugin-core for a patch release documenting CSS asset deduplication behavior in the Start manifest.
Core Deduplication Logic
packages/start-plugin-core/src/start-manifest-plugin/manifestBuilder.ts
Refactored dedupeNestedRoutePreloads to dedupeNestedRouteManifestEntries to handle both preloads and CSS assets. Added assets field to route shape, introduced appendAsset() helper with seenAssets tracking for CSS deduplication, enhanced route-tree lookup error handling, and improved state cleanup during traversal unwinding.
Test Suite Updates
packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts
Removed legacy deduplication tests. Added comprehensive test coverage for CSS deduplication across overlapping imported chunks, route-level CSS deduplication scenarios, route tree dedupe validation for both assets and preloads, and error handling for missing route references.

Sequence Diagram

sequenceDiagram
participant Builder as Start Manifest Builder
participant RouteTree as Route Tree
participant AssetCollector as Asset Collector
participant DedupeTracker as Dedupe Tracker
participant Output as Manifest Entry
Builder->>RouteTree: Traverse route tree starting at root
loop For each route in tree
RouteTree->>AssetCollector: Collect CSS assets from chunks
loop For each asset
AssetCollector->>DedupeTracker: Check asset identity
alt Asset not seen
DedupeTracker->>DedupeTracker: Add to seenAssets
AssetCollector->>Output: Include asset
else Asset already seen
AssetCollector->>AssetCollector: Skip duplicate
end
end
RouteTree->>Builder: Process child routes
end
Builder->>DedupeTracker: Clean up tracking state on unwind
DedupeTracker->>Builder: Complete deduplication
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 CSS stays neat, no repeats in sight,
Our manifest now dedupes just right!
From root to leaf, each asset tracked,
Stylesheet chains no longer cracked.
Shared styles once, not thrice—
TanStack Start keeps things nice! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title 'fix: dedupe repeated css assets in the start manifest' directly summarizes the main change: implementing CSS asset deduplication in the Start manifest.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix-dedupe-css

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud

nx-cloudBot commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 1867b8a

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded9m 32sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded29sView ↗

☁️ Nx Cloud last updated this comment at 2026-03-24 22:52:49 UTC

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

1 package(s) bumped directly, 3 bumped as dependents.

🟩 Patch bumps

PackageVersionReason
@tanstack/start-plugin-core1.167.8 → 1.167.9Changeset
@tanstack/react-start1.167.5 → 1.167.6Dependent
@tanstack/solid-start1.167.5 → 1.167.6Dependent
@tanstack/vue-start1.167.5 → 1.167.6Dependent

@github-actions

Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: 2e8d89fdb2de
  • Measured at: 2026-03-24T22:43:51.483Z
  • Baseline source: history:b1c0f4602df7
  • Dashboard: bundle-size history
ScenarioCurrent (gzip)Delta vs baselineRawBrotliTrend
react-router.minimal88.15 KiB0 B (0.00%)278.36 KiB76.56 KiB██████▁▁▁▁▁
react-router.full91.38 KiB0 B (0.00%)289.35 KiB79.21 KiB██████▁▁▂▂▂
solid-router.minimal35.80 KiB0 B (0.00%)108.26 KiB32.08 KiB██████▁▁▁▁▁
solid-router.full40.22 KiB0 B (0.00%)121.66 KiB36.01 KiB██████▁▁▂▂▂
vue-router.minimal53.78 KiB0 B (0.00%)154.49 KiB48.23 KiB██████▁▁▁▁▁
vue-router.full58.64 KiB0 B (0.00%)169.97 KiB52.52 KiB██████▁▁▃▃▃
react-start.minimal102.56 KiB0 B (0.00%)326.38 KiB88.64 KiB██████▁▁▂▂▂
react-start.full105.95 KiB0 B (0.00%)336.69 KiB91.49 KiB██████▁▁▂▂▂
solid-start.minimal49.87 KiB0 B (0.00%)154.45 KiB43.93 KiB██████▁▁▁▁▁
solid-start.full55.35 KiB0 B (0.00%)170.54 KiB48.62 KiB██████▁▁▂▂▂

Trend sparkline is historical gzip bytes ending with this PR measurement; lower is better.

@pkg-pr-new

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

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

@tanstack/eslint-plugin-router

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-fn-stubs

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

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

@tanstack/vue-start-client

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

@tanstack/vue-start-server

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

@tanstack/zod-adapter

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

commit: 1867b8a

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts (1)

379-379: Avoid new any escape hatches in the added tests.

These fixtures are already structurally typed, so the new any casts turn off strict-mode checks around the exact manifest shapes this suite is supposed to pin down. asset.attrs?.href is enough here, and the route tree literals can be typed via Parameters<typeof buildStartManifest>[0]['routeTreeRoutes'] instead of as any.

As per coding guidelines, "Use TypeScript strict mode with extensive type safety".

Also applies to: 455-455, 710-710, 827-827, 931-931, 1040-1040

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts`
at line 379, Replace the newly added "any" casts in the tests so they don't
bypass strict typing: change occurrences like assets.map((asset: any) =>
asset.attrs.href) to safely access the href via asset.attrs?.href, and remove
"as any" on route tree literals by typing them using Parameters<typeof
buildStartManifest>[0]['routeTreeRoutes']; update the test assertions to use the
safe nullable access and strongly-typed routeTreeRoutes to preserve strict-mode
checks (addresses the assets mapping and the route tree literals referenced in
manifestBuilder.test.ts).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/start-plugin-core/src/start-manifest-plugin/manifestBuilder.ts`:
- Around line 34-36: The dedupe pass currently collapses items in
DedupeRoute.assets using getAssetIdentity which only keys by href/src/rel/type
and thus incorrectly merges distinct tags that share a URL; update the dedupe to
operate only on stylesheet link tags (e.g., filter route.assets for
RouterManagedTag entries that are link elements with rel==='stylesheet' or
ManifestAssetLink-like items) before applying getAssetIdentity, leaving
non-stylesheet assets untouched, or alternatively replace the identity function
with one that includes full attributes (media, crossorigin, integrity, nonce)
and only apply that identity when processing generic route.assets; adjust the
logic in manifestBuilder.ts where DedupeRoute and route.assets are handled (also
mirror the same change at the other occurrences noted around the other blocks)
so only CSS link dedupe is scoped.
---
Nitpick comments:
In
`@packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts`:
- Line 379: Replace the newly added "any" casts in the tests so they don't
bypass strict typing: change occurrences like assets.map((asset: any) =>
asset.attrs.href) to safely access the href via asset.attrs?.href, and remove
"as any" on route tree literals by typing them using Parameters<typeof
buildStartManifest>[0]['routeTreeRoutes']; update the test assertions to use the
safe nullable access and strongly-typed routeTreeRoutes to preserve strict-mode
checks (addresses the assets mapping and the route tree literals referenced in
manifestBuilder.test.ts).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 948ab611-e516-42f3-8290-fac832bf286c

📥 Commits

Reviewing files that changed from the base of the PR and between 2e8d89f and 1867b8a.

📒 Files selected for processing (3)
  • .changeset/fair-rivers-drum.md
  • packages/start-plugin-core/src/start-manifest-plugin/manifestBuilder.ts
  • packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts

Comment on lines +34 to +36
type DedupeRoute = {
preloads?: Array<ManifestAssetLink>
assets?: Array<RouterManagedTag>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Scope the nested asset dedupe to stylesheet links.

route.assets can already carry arbitrary RouterManagedTags, but this new pass dedupes the whole array with getAssetIdentity. That identity only keys link/script tags by href/src/rel/type, so distinct assets that share a URL but differ by media, crossorigin, integrity, nonce, etc. will collapse here. Since this PR is only trying to remove duplicate CSS, the safer fix is to dedupe stylesheet links only, or switch to a full-attrs identity before applying it to generic route assets.

💡 One way to keep the dedupe scoped to CSS
 if (routeAssets && routeAssets.length > 0) {
let dedupedAssets: Array<RouterManagedTag> | undefined
for (let i = 0; i < routeAssets.length; i++) {
const asset = routeAssets[i]!
+ const isStylesheetLink =+ asset.tag === 'link' && asset.attrs?.rel === 'stylesheet'++ if (!isStylesheetLink) {+ if (dedupedAssets) {+ dedupedAssets.push(asset)+ }+ continue+ }+
const identity = getAssetIdentity(asset)
if (seenAssets.has(identity)) {
if (dedupedAssets === undefined) {
dedupedAssets = routeAssets.slice(0, i)
@@
if (routeAssets) {
for (let i = routeAssets.length - 1; i >= 0; i--) {
- seenAssets.delete(getAssetIdentity(routeAssets[i]!))+ const asset = routeAssets[i]!+ if (asset.tag === 'link' && asset.attrs?.rel === 'stylesheet') {+ seenAssets.delete(getAssetIdentity(asset))+ }
}
}

Also applies to: 566-591, 619-623

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/start-plugin-core/src/start-manifest-plugin/manifestBuilder.ts`
around lines 34 - 36, The dedupe pass currently collapses items in
DedupeRoute.assets using getAssetIdentity which only keys by href/src/rel/type
and thus incorrectly merges distinct tags that share a URL; update the dedupe to
operate only on stylesheet link tags (e.g., filter route.assets for
RouterManagedTag entries that are link elements with rel==='stylesheet' or
ManifestAssetLink-like items) before applying getAssetIdentity, leaving
non-stylesheet assets untouched, or alternatively replace the identity function
with one that includes full attributes (media, crossorigin, integrity, nonce)
and only apply that identity when processing generic route.assets; adjust the
logic in manifestBuilder.ts where DedupeRoute and route.assets are handled (also
mirror the same change at the other occurrences noted around the other blocks)
so only CSS link dedupe is scoped.

@codspeed-hq

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 6 untouched benchmarks


Comparing fix-dedupe-css (1867b8a) with main (b1c0f46)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (2e8d89f) during the generation of this report, so b1c0f46 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@schiller-manuel
schiller-manuel merged commit ed9c43d into mainMar 24, 2026
17 checks passed
@schiller-manuel
schiller-manuel deleted the fix-dedupe-css branch March 24, 2026 23:00
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.

1 participant

@schiller-manuel