-
Notifications
You must be signed in to change notification settings - Fork 12k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Creating unnecessary excessive chunks #27715
Comments
Are you encountering a Core Web Vitals regression in a production deployed application? Also, if possible, please consider using the latest version of Angular (currently 18.0). Earlier versions can potentially inject a large amount of Regarding HTTP/2, any browser/device that does not have HTTP/2 will also not support running an Angular application that uses a supported major version of Angular as well as many unsupported versions. Regardless of bundling strategy, HTTP/2 is an effective means to improving CWV metrics. As to barrel file usage, these have long-term been a source of problems when attempting to optimizing code especially when interoperating correctly with potentially side effectful modules and/or top-level await usage. Additionally, depending on what version of Angular was in use previously for the application, the AOT compiler itself may be involved as well. Older compiler versions rewrote component imports directly to the component implementation bypassing any intermediate re-exports. This has since been removed with one of the reasons being execution correctness. However, we are planning on some potential improvements in this area for the 18.x timeframe. Barrel file usage also appears to be the main underlying cause for the issue as described above so such improvements may be an effective mitigation in this case. |
Yes, the regression is measured on a production environment. With Crux and other RUM data.
In the worse case we have seems an approximate ~2 second regression on LCP. But please take into account that this is a large application with a lot of moving pieces. There are other factors there that are probably contributing to those numbers. Are other large applications experiencing these regressions? The minimal reproduction i have build illustrates the issue but a that scale the impact will be hidden by variability. If its a question of proving the performance degradation I can scale the minimal reproduction, deploy it and run performance test on it. What i would do is:
I would be able to scale the minimal reproduction by increasing the bundle size of each component with random strings and
I agree, using the latest supported version when possible is best. Angular v18 was release 6 days ago on 2024-05-20. The demo is using v17.3 and the real app is on v17.1. Currently v18 is not on the list of Actively supported versions
We are using HTTP/3 So i just wanted to mention that we are aware of the fact that if we where using HTTP/1 this change would have a massive negative impact on LCP. But we are indeed using HTTP/3
I am generally not a fan of barrel files. As mentioned in the issue, by doing that I might be removing some of the esbuild compilers ability to tree shake my code effectively.
I am not sure about that. The idea of strategically placing barrels files to optimize how the bundler chunks code seems off to me. It seems like it will be prone to errors and may produce harder to maintain code. That all being said using libs will produce barrel files, and so i do see how they can be necessary for some usages. But I don't think this is a valid solution for large scale applications.
I am using barrel files to opt out of the optimization here, i would say this is my hacky attempt to understand how this can be fixed. I would say the issue is the way esbuild currently handles code splitting and more specifically this statement:
I have opened an issue on esbuild itself to discusses this and see if there is a potential solution there. |
Some people tried to get a minChunkSize option into esbuild, with no success yet, though. You could write a super crude post processor that merges all <1kb chunks and rewrites the import statements to use that chunk instead. Or do a final pass with rollup (which got an |
Could you try from inside your npm i -g rollup
rollup --format es -d dist/ --experimentalMinChunkSize 1000 -- main-25ISSONF.js with From digging a bit deeper, it seems like vite only uses esbuild for the dev mode, and uses rollup instead for the production build. A handful of people even go so far that they right now develop |
@sod the solution is nice and it allows me to use a custom chunking strategy. But the issue i am facing when rebundling with rollup is that i loose the sourcemaps. if rollup produces sourcemaps it will produce them based on the chunks as an input which makes it not useful. Do you know if there is a way i can tell rollup to merge the original sourcemaps instead of producing a completely new one? |
try https://github.com/maxdavidson/rollup-plugin-sourcemaps - can't confirm if it works, we don't use sourcemaps in production. |
@sod can you please provide an example of how you made the rollup command work in combination with the sourcemaps plugin? I tried |
We don't use sourcemaps in production. |
I see, currently we are more or less blocked in my team and are not able to the application builder, since as you can see in the attached screenshot the esbuild version generates way too many JS requests, which cause a significant performance downgrade. The rollup command that @sod kindly wrote in his comment removes ~60% of the chunks which is great 🎉 but it causes errors like this to our app in random places:
..which basically prevent some of our components from being loaded and makes some of its features unusable. I thought that possibly modifying the command to utilise the sourcemaps plugin might have some positive impact, but maybe I was just too desperate. Has anyone encountered a similar issue? |
@dimeloper I suppose you issue is related to circular dependencies created by rollup. This is because of you use The issue with preventing these circular dependencies is not trivial. And i have had partial success on applications with 200 or so initial bundles but the solution was flacky. However achiving 1 initial bundle using manualChunks does work fine. I am still in the process of publishing it but you can copy the executor into you project and it should work. Git Repo If you have any issues with that solution let me know, I only have half or so of it in that repo. Also if any one has a suggestion about how to balance the nodes without causing circular dependencies. My initial attempt was solving it like a bin packing problem with a Greedy algorithm. But now knowing that it requires considering the link between the nodes i am not sure what solution appropriate. |
In version 18.1, we introduced an experimental chunk optimizer. Has anyone tried it? If so, what were the results? For more context, please refer to this #27953 |
Hey @alan-agius4 I had tried that optimiser in my case, feel free to check out the full thread in the Angular discord channel for more details: https://discord.com/channels/748677963142135818/1260214060830556211/1260535860131270691 Long story short, the results weren't really helpful in my case, I had to rework my app and fine tune the rollup post build script to achieve a sensible amount of chunks (we still have the rollup step in our pipelines). |
@dimeloper, you mentioned that the environment variables are being ignored. I'm not familiar with how |
@alan-agius4 ok but I tried it like |
If you set both Note: To avoid overlapping logs, you may need to disable progress logging using If you don't see any changes in the chunk sizes, we'll need a reproduction to investigate further. |
The We are still stuck on webpack though, as I can't get the esbuild variant to perform as good. Webpack has some incredible features like:
(2) and (3) you kind of solved by partial hydration, but that is not yet supported without zone.js, so we are sticking to webpack right now. |
@alan-agius4 I tried it once again using your debugging hint. This is the output I am getting having the optimiser enabled:
As seen in the last line, it looks like the optimiser is enabled. However the amount of chunks remains unchanged (dist folder contains the normal build output and dist-optimized contains the one using the optimiser): Before switching to the application builder and ESBuild we used to have 326 chunks in total.... |
@dimeloper, unfortunately without a reproduction it’s hard to tell why the chunks count remained the same. |
@dimeloper But there is an easy fix, instead of providing it to the executor using the define options simply create a .env file and set the env there. |
Command
build
Is this a regression?
The previous version in which this bug was not present was
No response
Description
Disclaimer
This is not really a bug, as even if it worked differently with
webpack
, foresbuild
this is normal. However, since the default bundler is nowesbuild
some would consider this a regression.Required chunks references in dynamic imported code get split in to separate chunks
When splitting code, esbuild does not seems to properly take into account what is already statically imported (thus "required") and does not need to be split into a separate chunk.
To illustrate this lets consider an example where we have the following 5 standalone components:
AppComponent
(The root component which is not lazy loaded)Feature1Component
(A feature component lazy loaded via a dynamic import in the router)Feature21Component
(A feature component lazy loaded via a dynamic import in the router)Ui1Component
(A UI component imported statically, like a button)Ui2Component
(A UI component imported statically, like a button)Chunks without Shared Components
If the
AppComponent
has aRouterOutlet
which has separate pathsFeature1Component
andFeature21Component
it will generate a chunk for the code shared between these components (lets call it Common Chunk).In this case the build output is:
And if
Feature1Component
importsUi1Component
andFeature21Component
importsUi2Component
then code fromUi1Component
will be bundle inFeature1Component
and the code fromUi2Component
will be bundle inFeature2Component
.Chunks with Shared Components
However, if the
AppComponent
also importsUi1Component
andUi2Component
this code is now shared and will be split into separate chunks.Thus the build output will now be:
The issue is that
Ui1Component
andUi2Component
are now statically imported in theAppComponent
, and therefore belong in this common chunk. These chunks are statically imported in the "common" chunk and required for the application to bootstrap but are downloaded separately.As they are always downloaded, splitting them does not seems to have a benefit, when
Feature1Component
orFeature2Component
imports them, the request is cached and it should not make a difference if it over imports.However, splitting them does have a downside, it increases the load time. Even tho in this example the consequences of a couple of extra chunks is meaningless in larger apps the consequence is very real.
If the application produces 100 or 200 initial chunks this will have a significant impact on the initial load time and LCP.
Even tho the a modern browser using http3 or http2 uses multiplexing it will still have an significant overhead downloading so many small chunks. We are currently calling this
the chunk gap
:Additionally this issue will have a larger impact on older and slower devices.
Additional example of performance impact:
Avoiding Code Splitting Shared Code
It seems possible to avoid additional chunking in some cases by using barrel files.
For example if we create a barrel file that exports both
Ui1Component
andUi2Component
and only import them via that barrel file we are able to forceesbuild
to place the shared code into the "common" chunk.So using:
The build output will now be:
Note
In
esbuild
architecture documentation it explains that:This means that doing so will partially optout of tree shaking.
For example if we add a
Ui3Component
and aFeature3Component
.And we add the
Ui3Component
to the barrel file:If the
Ui3Component
is never imported anywhere it will not increase the initial bundle.However, if we reference the
Ui3Component
inFeature3Component
(loading theFeature3Component
in a lazy route), this will increase the initial bundle even ifUi3Component
is not used in theAppComponent
.Minimal Reproduction
https://github.com/ChristopherPHolder/ng-esbuild-demo
Exception or Error
Your Environment
Anything else relevant?
No response
The text was updated successfully, but these errors were encountered: