angular-cli icon indicating copy to clipboard operation
angular-cli copied to clipboard

i18n causes esbuild chunks to be cache-busted on each deploy

Open laurentgoudet opened this issue 6 months ago • 1 comments

Command

build

Is this a regression?

  • [x] Yes, this behavior used to work in the previous version

The previous version in which this bug was not present was

Webpack builder

Description

Turning on esbuild is causing us to experience a large increase in the number of static requests served to our users (+9B requests/mo / +60% increase), leading to a substantial impact on our CDN bill.

Image

Each spike in the graph above aligns with a deploy (no deploys on weekends), indicating that all assets appear to be cache-busted with each new build (whereas in practice only a fraction of the code is updated between versions).

Upon investigating where this could come from, I noticed that chunks with identical content between two build versions have different filename hashes, i.e., chunk-ASRRVFTX.js in the first one and chunk-MO3N5ZQM.js in the second one:

Image Image

If you look carefully, you can notice that the chunk contents are not exactly identical—they differ by the value of the i18n: comment. Looking into it, that i18n hash is the same for all the chunks (in any locale) across a build and appears to be a SHA256 of all the localized dictionaries.

Since at least a couple of strings are going to change across builds (we have ~14k strings in the app), this effectively means that the entire app is cache-busted on each deploy, leading to increased infrastructure costs and degraded client-side performance.

Minimal Reproduction

Minimal repro is available here: https://github.com/laurentgoudet/angular-i18n-esbuild-bug

Steps I performed:

  • ng new
  • ng add @angular/localize
  • Configured an fr locale in angular.json
  • Added some string in app.ts
  • Ran ng extract-i18n to extract the dictionary
  • Copied messages.xlf to src/locale/messages.fr.xlf
  • Ran ng build --localize to run the localized build
ng build --localize
Initial chunk files   | Names         |  Raw size | Estimated transfer size
main-BGVPYDPI.js      | main          | 221.57 kB |                61.67 kB
polyfills-25EJAOGH.js | polyfills     |  36.97 kB |                12.41 kB
styles-5INURTSO.css   | styles        |   0 bytes |                 0 bytes

                      | Initial total | 258.54 kB |                74.08 kB

Application bundle generation complete. [1.794 seconds]
  • Observed that i18n: hash is the same between polyfills-<foo>.js & main-<foo>.js bundles in both en-US & fr
$ rg -l i18n:cf2ec5733a24b255a5f4c5407a5cd5747f51614f533bf44f15ae320960b64048 dist                                                                                          
dist/test-esbuild-i18n/browser/en-US/polyfills-PJEY6MCR.js
dist/test-esbuild-i18n/browser/fr/polyfills-PJEY6MCR.js
dist/test-esbuild-i18n/browser/en-US/main-YXPGXXTL.js
dist/test-esbuild-i18n/browser/fr/main-YXPGXXTL.js
  • Add a translation for in the fr dictionary (src/locale/messages.fr.xlf)

  • Run ng build --localize again

  • Observe that the filename hashes have changed

ng build --localize
Initial chunk files   | Names         |  Raw size | Estimated transfer size
main-YXPGXXTL.js      | main          | 221.57 kB |                61.67 kB
polyfills-PJEY6MCR.js | polyfills     |  36.97 kB |                12.45 kB
styles-5INURTSO.css   | styles        |   0 bytes |                 0 bytes

                      | Initial total | 258.54 kB |                74.13 kB

Application bundle generation complete. [1.426 seconds]

Exception or Error


Your Environment

ng version

     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/


Angular CLI: 20.0.5
Node: 22.15.0
Package Manager: npm 10.9.2
OS: darwin arm64

Angular: 20.0.6
... common, compiler, compiler-cli, core, forms, localize
... platform-browser, router

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.2000.5
@angular-devkit/core         20.0.5
@angular-devkit/schematics   20.0.5
@angular/build               20.0.5
@angular/cli                 20.0.5
@schematics/angular          20.0.5
rxjs                         7.8.2
typescript                   5.8.3
zone.js                      0.15.1

Anything else relevant?

No response

laurentgoudet avatar Jul 08 '25 19:07 laurentgoudet

We have deployed a workaround on July 14th to remove the i18n hash from the esbuild chunks:

diff --git a/node_modules/@angular-devkit/build-angular/src/tools/esbuild/application-code-bundle.js b/node_modules/@angular-devkit/build-angular/src/tools/esbuild/application-code-bundle.js
index 8167d55..96b29f1 100755
--- a/node_modules/@angular-devkit/build-angular/src/tools/esbuild/application-code-bundle.js
+++ b/node_modules/@angular-devkit/build-angular/src/tools/esbuild/application-code-bundle.js
@@ -240,8 +240,8 @@ function getEsBuildCommonOptions(options) {
     let footer;
     if (options.i18nOptions.shouldInline) {
         // Update file hashes to include translation file content
-        const i18nHash = Object.values(options.i18nOptions.locales).reduce((data, locale) => data + locale.files.map((file) => file.integrity || '').join('|'), '');
-        footer = { js: `/**i18n:${(0, node_crypto_1.createHash)('sha256').update(i18nHash).digest('hex')}*/` };
+        // const i18nHash = Object.values(options.i18nOptions.locales).reduce((data, locale) => data + locale.files.map((file) => file.integrity || '').join('|'), '');
+        // footer = { js: `/**i18n:${(0, node_crypto_1.createHash)('sha256').update(i18nHash).digest('hex')}*/` };
     }
     return {
         absWorkingDir: workspaceRoot,
Image

However, said workaround had a limited effect, as in practice most of the ~1150 chunks seems to have import chains leading back to a few (changing) parents, causing these to be cache-busted (due to the import names), even without the i18n hash.

Image Image

I think what makes it worse for us is that we have the service worker enabled, as all the (changed) chunks will be re-fetched on each deploy: esbuild smaller chunks strategy might be efficient on initial load (from a Web performance PoV) but in term of requests/bandwidth utilization (coupled with the Angular SW), we are still seeing a net >2x increase.

Interestingly as well - but not surprising - the overhead of HTTP headers (in blue below) is much larger compared to Webpack's "less chunks for bigger" strategy (before June 29th).

Image

Anyway, we are going to revert back to Webpack as the cost increase is fairly large at our scale (~$7k/mo extra) - I suspect most Angular users are running at a much smaller scale & won't be billed on the number of request by their CDN provider, i.e. are not impacted by that issue.

laurentgoudet avatar Jul 21 '25 09:07 laurentgoudet