Skip to content

"application" builder: chunk optimizer spends minutes in bundleOutputToEsbuildMetafile because renderedLength is re-read for every input (rolldown copies module code on each access) #34044

Description

@roman-garcia

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

In our relatively large application (initial bundle 18 MB raw, about 200 JS output files, and 8000 metafile inputs) a production ng build takes 234s, of which about 180s is the OPTIMIZE_CHUNKS phase (NG_BUILD_DEBUG_PERF=1). Rolldown itself accounts for about 3s of that. The remaining 157s is spent in bundleOutputToEsbuildMetafile in packages/angular/build/src/builders/application/chunk-optimizer.ts.

The cause is this loop:

for (const [moduleId, renderedModule] of Object.entries(chunk.modules)) {
  ...
  for (const [originalInputPath, originalInputInfo] of Object.entries(originalOutputEntry.inputs)) {
    const proportion = originalInputInfo.bytesInOutput / totalOriginalBytesInModule;
    const newBytesInOutput = Math.floor(renderedModule.renderedLength * proportion);
    ...
  }
}

renderedModule.renderedLength is read once per input of the module but it is an uncached getter:

// rolldown/dist/shared/bindingify-input-options-*.mjs
function transformToRenderedModule(bindingRenderedModule) {
  return {
    get code() { return bindingRenderedModule.code; },
    get renderedLength() { return bindingRenderedModule.code?.length || 0; },
    ...
  };
}

The bindingRenderedModule.code getter copies the module's entire rendered code from Rust into a fresh JS string at every read. In the chunk optimizer each "module" is a whole esbuild output chunk, so for our main chunk the inner loop performs 3,588 copies of a 17 MB string, roughly 62 GB of string transfer.

Reading renderedLength once per module before the loop fixes it:

const renderedLength = renderedModule.renderedLength;
for (const [originalInputPath, originalInputInfo] of Object.entries(originalOutputEntry.inputs)) {
  const proportion = originalInputInfo.bytesInOutput / totalOriginalBytesInModule;
  const newBytesInOutput = Math.floor(renderedLength * proportion);
  ...
}

With sourceMap: true:

OPTIMIZE_CHUNKS took about 180s before, and 4s after.
The total build went from 234s to 55s, which is huge.

I didn't find any difference in the output.

Minimal Reproduction

The cost is proportional to the number of inputs in a chunk and its size, so it needs a pretty large app to be visible. Any project whose main chunk has a few thousand input files and is several MB will show it.

  1. ng new repro (or any application using @angular/build:application / @angular-devkit/build-angular:application).
  2. Make the main bundle large with many inputs: import a few thousand small modules, a couple of large libraries, and at least 3 lazy routes so the chunk optimizer runs (default threshold NG_BUILD_OPTIMIZE_CHUNKS=3).
  3. NG_BUILD_DEBUG_PERF=1 ng build --configuration=production
  4. Compare DURATION[OPTIMIZE_CHUNKS] with and without the one-line change above (or with NG_BUILD_OPTIMIZE_CHUNKS=false).

For a direct measurement without a repro app, wrapping the bundleOutputToEsbuildMetafile call in process.hrtime shows it accounts for essentially the whole OPTIMIZE_CHUNKS duration.

Exception or Error

[PLUGIN_TIMINGS] Your build spent significant time in plugin `angular-bundle`. See https://rolldown.rs/reference/InputOptions.checks#plugintimings for more details.

DURATION[OPTIMIZE_CHUNKS]: 179.393928137s
Application bundle generation complete. [234.194 seconds]


With per-phase timers added inside `optimizeChunks`:


[CHUNK-OPT] bundle.generate(): 3.226s
[CHUNK-OPT] bundle.close(): 0.000s
[CHUNK-OPT]   metafile: chunk.modules access 0.002s, module iters 184, input iters 3770, inputs in metafile 7911, outputs 1150
[CHUNK-OPT] bundleOutputToEsbuildMetafile: 157.214s
[CHUNK-OPT] push optimized chunks (code/map access + toString): 0.730s
DURATION[OPTIMIZE_CHUNKS]: 161.235368804s

Your Environment

Angular CLI       : 22.1.7
Angular           : 22.1.5
Node.js           : 24.16.0
Package Manager   : yarn 1.22.22
Operating System  : linux x64

@angular-devkit/build-angular     22.1.7
@angular/build                    22.1.7
@angular/cli                      22.1.7
@angular/compiler-cli             22.1.5
ng-packagr                        22.0.0
typescript                        6.0.3
zone.js                           0.15.0

rolldown (dependency of @angular/build)  1.2.0
esbuild                                  0.28.2

Anything else relevant?

  • Build options: optimization: true, sourceMap: true, outputHashing: all, namedChunks: false. The slowdown does not depend on sourcemaps though.
  • Happy to open a PR with the fix.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions