Skip to content

fix(build): deduplicate shared CSS chunks between prerender and client builds#16447

Draft
aubergene wants to merge 1 commit intowithastro:mainfrom
aubergene:fix/css-dedup-shared-chunks
Draft

fix(build): deduplicate shared CSS chunks between prerender and client builds#16447
aubergene wants to merge 1 commit intowithastro:mainfrom
aubergene:fix/css-dedup-shared-chunks

Conversation

@aubergene
Copy link
Copy Markdown

Changes

  • Fixes duplicate CSS <link> tags when a CSS module (e.g. base.css) is shared between a client:load (hydrated) component and a client:only component.
  • When two islands share a CSS import, Vite's client build places the shared CSS into its own chunk. Because that chunk's modules record contains only the CSS module ID — not any JS component modules — the existing deduplication guard (shouldDeleteCSSChunk) never fired, leaving a redundant <link> in the page HTML even though the prerender build had already emitted and linked the same styles.
  • Fix: in plugin-css.ts, before the existing per-page assignment logic, we now check whether every CSS module in a client-build chunk is already tracked in internals.cssModuleToChunkIdMap (populated by the prerender build). If so, the redundant client-side CSS assets are deleted immediately — regardless of whether the chunk also contains component JS.

Testing

Added a new integration test (test/css-ssr-client-dedup.test.ts) with a dedicated fixture (fixtures/css-ssr-client-dedup/) that reproduces the exact scenario:

  • Nav (client:load) — imports base.css + nav.css; processed in both the prerender and client builds.
  • Share (client:only) — imports base.css + share.css; processed only in the client build.

base.css being shared between both islands previously caused a redundant <link> tag. The tests assert (a) no CSS file's content is a subset of another linked file, and (b) all component styles are present exactly once on the page.

All existing CSS integration tests continue to pass (46/46).

Docs

No user-facing API or behaviour change — this is a build output bug fix. No documentation update needed.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 22, 2026

🦋 Changeset detected

Latest commit: 9cd7022

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the pkg: astro Related to the core `astro` package (scope) label Apr 22, 2026
@aubergene aubergene force-pushed the fix/css-dedup-shared-chunks branch from 10d185f to 2a3534a Compare April 22, 2026 11:55
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 22, 2026

Merging this PR will not alter performance

✅ 18 untouched benchmarks


Comparing aubergene:fix/css-dedup-shared-chunks (9cd7022) with main (b2d8eb3)1

Open in CodSpeed

Footnotes

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

@aubergene aubergene marked this pull request as draft April 22, 2026 12:28
@aubergene aubergene force-pushed the fix/css-dedup-shared-chunks branch from a0871f7 to 3f95157 Compare April 22, 2026 16:56
…t builds

When a CSS module is shared between a hydrated (client:load) component and
a client-only (client:only) component, Vite places it in a separate shared
CSS chunk. That chunk's modules record contains only the CSS module ID, not
any JS component modules, so the existing shouldDeleteCSSChunk guard returned
false — leaving the chunk in the client bundle even when allCssInSSR was true
(i.e. the prerender build had already emitted and linked that CSS).

The result was the same stylesheet linked twice on the page: once from the
prerender build's combined chunk and once from the client build's shared chunk.

Fix: inside the allCssInSSR branch, distinguish between pure CSS chunks (the
modules record contains only CSS virtual module IDs, no JS) and mixed JS+CSS
chunks. Pure CSS chunks are safe to delete unconditionally because they carry
only shared CSS the prerender build already emitted. Mixed chunks keep the
existing shouldDeleteCSSChunk() guard so that CSS is not removed when a
component is also used via normal SSR rendering on a different page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@aubergene aubergene force-pushed the fix/css-dedup-shared-chunks branch from 50a78a4 to 9cd7022 Compare April 23, 2026 13:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg: astro Related to the core `astro` package (scope)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant