Optimize Static Builds: Tree-shake Unused Components in CMS Page Builder Patterns #1054
Replies: 7 comments 7 replies
-
We would like to see that in dynamic SSR rendering as well and would even waive the streaming option since every page is cached through our cdn for an hour anyway. |
Beta Was this translation helpful? Give feedback.
-
Definitely interested in this, today I had a video component in my articles page, which added 240kb to the bundle because it used mux-player library, even if the article didn't have one. Changed this to dynamic imports with intersection observer, which solved it, but almost slipped past me. |
Beta Was this translation helpful? Give feedback.
-
Hi @moritzlaube, I don't think I quite understand what you are asking for. In particular:
What do you mean here? What is a "page bundle"? |
Beta Was this translation helpful? Give feedback.
-
For better context. Here are the two code examples that you can find on Stackblitz as well.
|
Beta Was this translation helpful? Give feedback.
-
This comment got a bit longer than I expected - I am not experienced in web-framework design or implementation, so my opinions should be taken with a grain of salt, but I wanted to share my perspective both to emphasise that the current issues with the bundling behaviour are impactful to my current project and provide some more details into the issues this current behaviour causes. I appreciate the hard work maintainers put into this great project and am looking forward to its further development, sorry if my tone comes across as overly critical This would be an important improvement for me - including every imported script (and imported CSS files) from every component on every page severely negates the performance (and DX) advantages of using Astro in the first place. Having dug into this a little to try and understand what's going on, I remain a bit confused as to why frontmatter imports are included in the client-side bundle in the first place. This is definitely not intuitive behaviour in all contexts I have encountered, not just when using a CMS. The user-abstraction of the frontmatter is that it's all serverside code, and that it is used to generate the HTML given to the client, but is not itself exposed to the client. Broadly speaking, I don't think it would ever be intuitive that importing an Astro component would cause that components scripts to run on the client even if the component is not actually rendered in the template. It significantly impacts the intuitive expectation of encapsulation, as a user I would hope that a components scripts, styles, etc, are attached to the component, and are included only when it does. Experience with rendering syntaxes with similar component-ized templating such as JSX would contribute to that expectation. For example, in the following hypothetical Astro components
I suspect most users would expect that any scripts included in Script bundling is an important performance improvement, but in this case the implementation of the optimisation is severely harming both performance and developer experience - Im having to rewrite scripts under the new expectation that they will in fact run on every page. Optimisations should impact development abstractions as little as possible, otherwise the abstraction can end up obfuscating behaviour more than simplifying it. The "scripts are only included once" transformation is already a slight leak in this abstraction, but requires fairly minimal code changes to work around and is likely not something that could be re-aligned without significant performance compromises, but in this case, the intuitive behaviour would also be the more performant behaviour. I initially discovered this behaviour when debugging an unexpectedly large bundle size, discovering that a large library necessary for only a single page was being included in every page, slowing down the whole site rather than just the single page - again, negating one of the core reasons to use astro in the first place. CSS (or SCSS, etc) imports are also frustrating in this regard - it is not possible as far as I can identify to import a CSS file for a specific component, making "granular, per-component styles" via imported files non-functional, if that component is imported through any chain to any components that are dynamically selected by the CMS. If it is, that CSS import will be included in every page. Being able to include a CSS file in the produced HTML just by importing it in the frontmatter is also behaviour that is somewhat misaligned with the rest of Astro's design implications, but it's convenient enough to keep so long as that convenience doesn't conflict with actual functionality. An encapsulating frustration to all of this is that none of this behaviour is documented anywhere I can find. The docs are fairly vague on the specifics of import logic, nor do any of the CMS guides mention the severe drawbacks induced by the dynamic rendering logic required to use them. I have been struggling with various issues for some time now before finally tracking all of them back to this core issue. The various pieces of odd behaviour this can cause are unique enough to be impossible to search for until you have properly tracked down and identified this as the root. Until/unless a release resolves these issues, I strongly feel the documentation should include warnings about this behaviour in several places. Generally it seems that while CMS integrations feature quite heavily in the documentation, various Astro limitations result in a significantly different set of rules, behaviours and constraints when using very dynamic content with either static or server-side rendering, few of which are adequately covered. |
Beta Was this translation helpful? Give feedback.
-
Could it be that this is already fixed by version 5.5.2? Having a CMS where the clients can choose different blocks is totally a use case that is important for me, so I wanted to make sure it works correctly. I stumbled upon this proposal while checking it out and was worried that ALL my components would make it to the build even if not used. I just started with Astro, so please have some patience but if I do...
while having this data:
and 3 TestLog components (TestLog, TestLog2 and TestLog3) that look like this (but logging 1, 2 or 3):
I correctly see these logs:
So, no Logger3 Did I miss something, or is this now working correctly and it wasn't before? Thanks a bunch! |
Beta Was this translation helpful? Give feedback.
-
I looked a bit into the vite plugins, that handle the CSS Splitting in Astro. Looking into the current code of the What if it would be a bit different, so that it checks, if the chunk is part of different pages. If the chunk is used on different pages, it can be in separate CSS file. I investigated a bit together with AI to make some sense of the CSS Plugin and this is what could be a suggestion to change the chunking algorithm. // the following script does the following:
// - If CSS is only used by a single page → it will end up in a page-specific CSS chunk, just like before.
// - If CSS is imported by multiple pages → it will be placed in a dedicated shared chunk (named using a hash).
// - This prevents every page from being assigned all shared styles directly.
extendManualChunks(outputOptions, {
after(id, meta) {
if (!isBuildableCSSRequest(id)) return;
// Client-Build: use same module id as on the server build.
if (options.target === 'client') {
return internals.cssModuleToChunkIdMap.get(id)!;
}
const ctx = { getModuleInfo: meta.getModuleInfo };
const parentPages = Array.from(getParentModuleInfos(id, ctx)).filter((info) =>
moduleIsTopLevelPage(info)
);
let chunkId: string;
if (parentPages.length > 1) {
// Shared CSS: custom dedicated chunk
chunkId = assetName.createNameHash(id, parentPages.map((p) => p.id), settings);
} else if (parentPages.length === 1) {
// Page specific CSS
chunkId = assetName.createSlugger(settings)(id, meta);
} else {
// Fallback: default name of the chunk
chunkId = assetName.createNameHash(id, [id], settings);
}
internals.cssModuleToChunkIdMap.set(id, chunkId);
return chunkId;
},
}); But, this change would only change the CSS part. The page will still get all the styles applied. I didn’t find the place in the code, where the CSS is applied to the pages. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Body
Summary
When building static sites with a CMS page builder approach, where pages are dynamically composed of a library of components, Astro currently includes all potential components in every page bundle, even when only a subset is actually used. This RFC proposes build optimizations for static sites to only include components that are actually used in the final bundle, either through static imports with tree-shaking or optimized dynamic imports.
Background & Motivation
Modern headless CMSs like Strapi, Prismic, Directus or even WordPress with the Gutenberg Blocks offer page builder capabilities where content editors can freely compose pages from a predefined set of components. For example, they can build a landing page by combining a hero section, feature list, testimonials, and a contact form – all through the CMS interface.
When building such sites with Astro (
output: 'static'
), the current behavior includes all available components in every page bundle, even though:This leads to unnecessarily bloated bundles in static sites, where pages might only use 3–4 components but include scripts and styles from, let's say, all 20-30 available components. While the current behavior makes sense for SSR where streaming requires knowing all possible scripts upfront, it seems unnecessary for static builds.
This issue was previously discussed in #4863 but was closed due to perceived technical constraints related to streaming. However, since streaming isn't relevant for static builds, this limitation should be revisitable.
Goals
Example
In my humble opinion, there are two possible approaches to implement this optimization, using either static or dynamic imports. Both approaches would solve the same problem of unnecessary component loading.
Current behavior can be seen in this Stackblitz example – although using an outdated Astro version, the issue still applies –, where even unused components are included in the final bundle.
Static Import Approach (Preferred)
Dynamic Import Approach (Alternative)
With either approach, Astro should:
The static import approach offers several advantages:
Both approaches would achieve the same goal: ensuring only used components are included in the final static build. The static import approach might be easier to implement since it leverages existing static analysis capabilities of bundlers.
This feature would be crucial for anyone building CMS-powered static sites where pages are dynamically composed through a page builder interface (which is really common when working with clients).
While my understanding of Astro's internal architecture is quite limited, I hope this proposal can spark a discussion about this important feature. And thank you for creating such an amazing tool!
Beta Was this translation helpful? Give feedback.
All reactions