Repository navigation
Description
Command
build
Is this a regression?
- Yes, this behavior used to work in the previous version
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, for esbuild this is normal. However, since the default bundler is now esbuild 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 a RouterOutlet which has separate paths Feature1Component and Feature21Component it will generate a chunk for the code shared between these components (lets call it Common Chunk).
In this case the build output is:
| Initial chunk files | Names |
|---|---|
| chunk-YIKH6UD4.js | - ("common") |
| main-V6UA4PHT.js | main |
| Lazy chunk files | Names |
|---|---|
| feature-1.component-AQEBZKLJ.js | feature-1-component |
| feature-2.component-EH7EGWEP.js | feature-2-component |
And if Feature1Component imports Ui1Component and Feature21Component imports Ui2Component then code from Ui1Component will be bundle in Feature1Component and the code from Ui2Component will be bundle in Feature2Component.
Chunks with Shared Components
However, if the AppComponent also imports Ui1Component and Ui2Component this code is now shared and will be split into separate chunks.
Thus the build output will now be:
| Initial chunk files | Names |
|---|---|
| chunk-YIKH6UD4.js | - ("common") |
| main-V6UA4PHT.js | main |
| chunk-WKA4CZR7.js | - ("ui-1-component") |
| chunk-XCJUHD2B.js | - ("ui-2-component") |
| Lazy chunk files | Names |
|---|---|
| feature-1.component-AQEBZKLJ.js | feature-1-component |
| feature-2.component-EH7EGWEP.js | feature-2-component |
The issue is that Ui1Component and Ui2Component are now statically imported in the AppComponent, 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 or Feature2Component 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 and Ui2Component and only import them via that barrel file we are able to force esbuild to place the shared code into the "common" chunk.
So using:
// ./ui/index.ts
export * from './ui-1.component';
export * from './ui-2.component';The build output will now be:
| Initial chunk files | Names |
|---|---|
| chunk-YIKH6UD4.js | - ("common") |
| main-V6UA4PHT.js | main |
| Lazy chunk files | Names |
|---|---|
| feature-1.component-AQEBZKLJ.js | feature-1-component |
| feature-2.component-EH7EGWEP.js | feature-2-component |
Note
In esbuild architecture documentation it explains that:
code splitting is implemented as an advanced form of tree shaking.
This means that doing so will partially optout of tree shaking.
For example if we add a Ui3Component and a Feature3Component.
And we add the Ui3Component to the barrel file:
// ./ui/index.ts
export * from './ui-1.component';
export * from './ui-2.component';
export * from './ui-3.component';If the Ui3Component is never imported anywhere it will not increase the initial bundle.
However, if we reference the Ui3Component in Feature3Component (loading the Feature3Component in a lazy route), this will increase the initial bundle even if Ui3Component is not used in the AppComponent.
Minimal Reproduction
https://github.com/ChristopherPHolder/ng-esbuild-demo
Exception or Error
Shared Code Required in a root component or a root node of the node graph is bundle together.
Your Environment
Angular CLI: 17.3.5
Node: 20.11.1
Package Manager: npm 10.2.4
OS: win32 x64
Angular: 17.3.5
... animations, cli, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router
Package Version
---------------------------------------------------------
@angular-devkit/architect 0.1703.5
@angular-devkit/build-angular 17.3.5
@angular-devkit/core 17.3.5
@angular-devkit/schematics 17.3.5
@schematics/angular 17.3.5
ng-packagr 17.3.0
rxjs 7.8.1
typescript 5.4.5
zone.js 0.14.4
Anything else relevant?
No response


Activity
clydin commentedon May 25, 2024
Are you encountering a Core Web Vitals regression in a production deployed application?
If so, can you please provide the before and after metric values?
Measurements should ideally be taken from production or production-equivalent deployments. Server and/or hosting configuration can have a significant effect on the metrics. The development server, especially, should not be used to evaluate loading performance as it is designed to optimize the edit/refresh cycle and not initial load.
Also, if possible, please consider using the latest version of Angular (currently 18.0). Earlier versions can potentially inject a large amount of
modulepreloadlink elements into the generated index HTML especially in SSR cases which can lead to performance regressions. The fix for this may be back-ported. However, preload generation can be disabled via configuration in these cases. Also, there are numerous improvements in other areas from which the application may benefit by upgrading.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.
ChristopherPHolder commentedon May 26, 2024
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.
sod commentedon May 27, 2024
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
experimentalMinChunkSizeoption) in the meantime.sod commentedon May 28, 2024
Could you try from inside your
<app-name>/browserdirectory:with
main-25ISSONF.jsbeing your entry chunk? And measure how the output from rollup performs for your app? For our build it removes ~60% of the chunks and is even quite fast in doing so. The rebundled app runs perfectly fine.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
rolldownwhich is supposed to superseed esbuild+rollup for vite, so they have a single build tool that works for dev & prod. So if you invest time now to have a "post optimization step" via rollup, you may be able to swap it for rolldown in a year to get most of the added build time overhead back.ChristopherPHolder commentedon Jun 13, 2024
@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?
sod commentedon Jun 13, 2024
try https://github.com/maxdavidson/rollup-plugin-sourcemaps - can't confirm if it works, we don't use sourcemaps in production.
dimeloper commentedon Jul 9, 2024
@sod can you please provide an example of how you made the rollup command work in combination with the sourcemaps plugin?
I tried
rollup --format es -d ./ -- main-2ZWZBTDC.js -p rollup-plugin-sourcemapsbut didn't work for me. Obviously I'm doing something wrong.sod commentedon Jul 9, 2024
We don't use sourcemaps in production.
dimeloper commentedon Jul 9, 2024
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?
ChristopherPHolder commentedon Jul 9, 2024
@dimeloper I suppose you issue is related to circular dependencies created by rollup. This is because of you use
minChunkSizewhich is still experimental.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.
alan-agius4 commentedon Aug 30, 2024
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
dimeloper commentedon Aug 30, 2024
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).
alan-agius4 commentedon Aug 30, 2024
@dimeloper, you mentioned that the environment variables are being ignored. I'm not familiar with how
env-cmdfunctions, but typically, environment variables need to have their values defined as strings.dimeloper commentedon Aug 30, 2024
@alan-agius4 ok but I tried it like
NG_BUILD_OPTIMIZE_CHUNKS=1and likeNG_BUILD_OPTIMIZE_CHUNKS="1"- how should I declare it instead, so as to to see it working? And is there a log message or something during the build process that I can see and confirm that the optimiser was enabled?27 remaining items
imaksp commentedon Oct 3, 2025
Some updated stats from one of our app (@angular/build version 20.3.4):
Without rolldown optimization (default):
Total chunks count: 135
Initial total: 3.38 MB
With NG_BUILD_OPTIMIZE_CHUNKS:
Total chunks count: 99 (so its working now but it still has many <1kB and <2kb chunks)
Initial total: 3.46 MB (no chunks in initial bundle - all 99 are lazy but main bundle size has increased)
ChristopherPHolder commentedon Oct 13, 2025
I figure it would be worth adding this:
This is the impact of adding chunks.
Basically i rendered an app and each route displays basically the same content but the content is split by being referenced in dynamic entries
Azbesciak commentedon Nov 5, 2025
Is there any ETA to completely address that issue?
That does not look appealing, not to mention migrations are said to be seamless and rather improve the performance (as the whole esbuild migration was promising), whereas we have basic, rather not expected issues.
sefatanam commentedon Nov 27, 2025
chunk after chunk, the horrible thing for me is now I can't see the imitator name from chrome devtool(network tab) instead showing chunks_ASDF, does anyone know how to get rid of it ?
imaksp commentedon Jun 26, 2026
It seems ESBuild has issues with tree shaking too, it can't tree shake modern viem lib properly, so as a workaround we need to pull some of its sub-dependencies directly.
only 4 functions isAddress, formatUnits, getAddress, isAddressEqual were pulling
~200kBraw in our initial bundle. barrel import did not fix it either (from Claude: the viem/utils barrel drags in constants/abis, constants/contracts, errors/{rpc,abi,contract,transaction}, actions/public/call, parseTransaction, @noble/curves signing (secp256k1/weierstrass), and abitype)Also it seems
NG_BUILD_CHUNKS_ROLLDOWN=1has a reporting error, if we turn this on it significantly reduces Initial bundle size in build report , but actual diff was ~1% (for gzipped version)delagen commentedon Jul 16, 2026
Was ~1600 files, with NG_BUILD_CHUNKS_ROLLDOWN=1 now ~2400. Good optimization ;)
eggsys commentedon Sep 7, 2026
is this issue got solve by upgrading Angular version ?
ChristopherPHolder commentedon Sep 7, 2026
I dont think this is something that will get "fixed" as its not really a regression or a bug but a core decition in the bundler.
I wrote an article about it in case you want to read more about it https://push-based.io/article/faster-builds-slower-applications-optimizing-angulars-bundle-output
Also we opensourced an experimental optimization that in most cases should work better then the default optimizer used in case someone wants to try it out we are open to feedback https://www.rx-angular.io/docs/packages/rebundle
delagen commentedon Oct 13, 2026
@ChristopherPHolder I tried RxAngular/rebundle, it simply hangs forever in default configuration
ChristopherPHolder commentedon Oct 15, 2026
Can you provided some additional details about your project and how its configured?
Also an issue with a reproducible would be appreciated @delagen
https://github.com/rx-angular/rx-angular/issues
delagen commentedon Oct 15, 2026
@ChristopherPHolder it's enterprise project, so i cannot share.
Details that i can provide:
Plugin added via @angular-builders/custom-esbuild with default config to angular-cli angular.json
Actual angular 22 project
softhouse2341 commentedon Oct 22, 2026
This is worst design decision yet. And it looks like Angular team don't care because this issue is there for many angular versions. Also it looks like NG_BUILD_OPTIMIZE_CHUNKS changes hashes in too many chunks
ChristopherPHolder commentedon Oct 23, 2026
If there is anyway you can provide a reproducible we would be happy to fix you issue
i can also integrate more debugging tools into it so we can find why its not functioning for you @delagen
ChristopherPHolder commentedon Oct 23, 2026
I see more improvements related to the initial static closure are landing #34408 (comment)
@clydin do you plan to implement this feature for internal static closures from a dynamic route? The PR you merged only seems to deal with the top layer which for apps that follow a shell pattern is simply not enough.
imaksp commentedon Oct 26, 2026
It seems that PR is just a fix for rollup version parity not a new improvement.