Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-css-dedup-shared-chunks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes duplicate CSS being emitted when a CSS module is shared between a hydrated (`client:load`) component and a client-only (`client:only`) component. Previously, the shared CSS chunk produced by the client build was kept even when all its CSS had already been emitted by the prerender build, resulting in the same styles being linked twice on the page.
26 changes: 22 additions & 4 deletions packages/astro/src/core/build/plugins/plugin-css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,28 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
internals.cssModuleToChunkIdMap.has(moduleId),
);

if (allCssInSSR && shouldDeleteCSSChunk(allModules, internals)) {
// Delete the CSS assets that were imported by this chunk
for (const cssId of meta.importedCss) {
delete bundle[cssId];
if (allCssInSSR) {
// Determine whether this is a pure CSS chunk (no JS component modules).
// Vite creates these when multiple client islands share the same CSS
// import — the shared CSS ends up in its own chunk whose `modules`
// record contains only CSS module IDs. shouldDeleteCSSChunk() returns
// false for such chunks because it looks for JS component module IDs,
// so those shared-CSS chunks were previously left in the client bundle
// and linked as a redundant <link> tag even when the prerender build
// had already emitted the same CSS.
//
// For mixed chunks (JS + CSS) we keep the shouldDeleteCSSChunk() guard
// so we don't accidentally remove CSS that a client:only component
// needs when the same CSS happens to appear in cssModuleToChunkIdMap
// only because an unrelated SSR-only component on a different page
// imported it.
const nonCssModules = allModules.filter((m) => !isCSSRequest(m));
const isPureCssChunk = nonCssModules.length === 0;

if (isPureCssChunk || shouldDeleteCSSChunk(allModules, internals)) {
for (const cssId of meta.importedCss) {
delete bundle[cssId];
}
}
}
}
Expand Down
Loading