Tools to optimize build outputs from Angular when using Esbuild.
The reachability strategy attempts to optimize the bundle as much as possible without any significant increase in bundle size.
It does this by traversing the imports and merging chunks based on the paths from which the code is reachable.
The Esbuild chunking algorithm considers each dynamic entry point, as its own entry point. Because it does not distinguish between entry points, it will optimize each entry point to reduce the amount of code required to load that entry point.
graph TD
main
dynamic
shared
main --> shared
dynamic --> shared
main -.-> dynamic
TODO add visual
However, in the context of single page applications like angular, there is only one entry point and the rest of the dynamic entry points cannot load or function outside that context. This means that we do not need to optimize for each entry point but instead optimize for the entry points considering only there reachability from the main entry point.
TODO add visual
graph TD
main
dynamic
shared
subgraph G1["main"]
main
shared
end
subgraph G2["dynamic"]
dynamic
end
G1 -.-> G2
G2 --> G1
This is achievable with rolldown and would usually be the default behaviour. However, there are a couple caviates and configurations that would have to be used.
TODO explain the config and why its necessary
Yet, this still does not work when rebundling an angular application, because of that we require analyzing the module graph ourselves and create an advance chunking strategy based on this strategy.
Important
This is still not published, we hope that we don't need to and angular will integrate something into the new build
system. This is already in progress with the experimental chunk optimizer:
NG_BUILD_OPTIMIZE_CHUNKS=1
It is currently exposed as an Esbuild Plugin allowing you to simply add it to you build target in NX and optionally specify the number of chunks you would like to output:
{
"build": {
"executor": "@nx/angular:application",
"options": {
"plugins": ["@ngx-build/esbuild-plugin"]
}
}
}By default, we optimize produce 6 initial chunks as use a greedy algorithm to make them approximately the same size. However, if you want to specify the number of chunks you would it to output you can pass it to the plugin:
{
"build": {
"executor": "@nx/angular:application",
"options": {
"plugins": [
{
"path": "@ngx-build/esbuild-plugin",
"options": {
"maxChunks": 3
}
}
]
}
}
}When migrating to Angular's new Build System you might have noticed that there is a significant difference in the initial chunks.
While Webpack originally bundled the initial chunks into a couple of named chunks:
- main.ts
- runtime.ts
- vendor.ts
- polyfills.ts
Esbuild on the other hand does not work in the same was and will many more initial unnamed chunks:
- main.ts
- polyfills.ts
- chunk.1.ts
- chunk.2.ts
- chunk.3.ts
- ...
It is important to recognize this is not an issue for most application and is only a problem when you have a very large
and dynamic application. The issue comes when what use to be 3 or 4 files has now become 200 files, all necessary to
bootstrap the application. This will cause a performance degradation for the initial user experience, which we like to
refer to as The Chunk Gap.
The degradation in performance is most likely caused by network thrashing as both the server and client are being
overwhelmed with excessive requests. And even tho HTTP/2 and HTTP/3 have a massive advantage over HTTP 1.1 we can
still see a clear degradation.
We are working on extending the docs, in the meantime here are some resources which might be helpful:
- refactor(@angular/build): add experimental chunk optimizer for production application builds
- Creating unnecessary excessive chunks
- Code splitting is creating many small unnecessary chunks
- Degraded Web Vitals (LCP, FCP) after switching a universal app to esbuild
- application builder generates many initial chunks
-- Compare output states of both bundles and identify where the additional sources are coming from.
-- Investigate potential issues with js transformer