Skip to content

Production build peak memory +53% after upgrading @angular/build 21.2.13 → 22.0.0 (same code, same esbuild version) #33368

Closed

Description

@t-barak

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

@angular/build 21.2.13 / Angular 21.2.15 / TS 5.9.3

Description

After upgrading to Angular 22, our production build's peak memory rose from ~15.5GB to ~23.8GB (+53%), which started OOM-killing our 16GB CI runners. A/B measurement on the identical commit of a large Nx monorepo (~7,400 TS
files, ~2,250 components, single application target), cold build cache, default workers, sampling combined RSS of the full node + esbuild process tree every 3s:
Toolchain │ Peak RSS │
@angular/build 21.2.13, Angular 21.2.15, TS 5.9.3 │ 15.5GB │
@angular/build 22.0.0, Angular 22.0.0, TS 6.0.3 │ 23.8GB │
@angular/build 22.0.1, Angular 22.0.0, TS 6.0.3 │ 22.3GB │

  • esbuild is 0.28.0 in both setups (unchanged).
  • The application code is identical in both runs (the v21 build needed only ignoreDeprecations adjusted in tsconfig).
  • 22.0.1's stylesheet-bundler leak fixes (fix(@angular/build): prevent concurrent stylesheet bundling esbuild context leaks #33318) recover ~1.5GB but the bulk of the increase remains.
  • NG_BUILD_MAX_WORKERS=2 makes peak memory worse (31.8GB), not better.
  • Caveat: TypeScript necessarily moved 5.9.3 → 6.0.3 with the Angular major, so we cannot separate the TS 6 share of the increase from @angular/build 22 itself.
  • Symptom on constrained machines: host OOM killer fires (or esbuild dies with fatal error: all goroutines are asleep - deadlock! when Node's --max-old-space-size exceeds physical RAM).

Minimal Reproduction

Private repo, so I can't share a reproduction, but happy to run instrumented builds / --stats-json / heap profiles on request.

Exception or Error


Your Environment

Node 24.16.0, npm 11.x, macOS 27 (arm64) + Linux CI, Nx 21.3.2

Anything else relevant?

No response

Activity

  1. added this to the needsTriage milestone on Jun 12, 2026
  2. ffried commented on Jun 17, 2026

    @ffried

    I've run into the same issue but already with Angular 21.2.15.
    After we upgraded from @angular/build 21.2.13 -> 21.2.15 (Angular 21.2.15 -> 21.2.17), our Docker build in GitHub Actions (ubuntu-latest with 8GB) as well as locally in Docker Desktop (limited to 8GB) started failing with either Docker OOM or within esbuild fatal error: all goroutines are asleep - deadlock!.
    After increasing the RAM limit to 16 GB for our local Docker Desktop environment and using a larger runner in GitHub, the build succeeds again.

  3. dmitry-stepanenko commented on Jun 17, 2026

    @dmitry-stepanenko
    Contributor

    Environment: Angular 22 (@angular/build@22.0.0), CI on ubuntu-latest (2 CPUs, 7.9GB RAM). App is in the Nx monorepo

    We tested with a real production app (1898 lazy chunks) and a mock app (21 / 22) with a large number of dummy routed components to reproduce the issue in isolation (2500 chunks)

    Observed: Builds started OOMing after migrating from ng21. Error: The operation was canceled. Even with NG_BUILD_OPTIMIZE_CHUNKS=false the build fails — system reaches ~7.8GB (98%) leaving ~160MB free, which is not enough for post-build operations.

    We added phase logging + system memory monitor (/proc/meminfo) to execute-build.js to capture timing and memory at each build phase.

    Key data measured on CI (mock app, 2500 chunks):

    ng21 ng22
    bundleAll:done sys 6913MB 6254MB (ng22 lower, actually confusing)
    optimizeChunks duration 2.3s 19.1s (8x slower)
    heap spike during opt +20MB +1627MB
    sys spike during opt +533MB +1490MB
    peak sys (opt ON) 7446MB (free=491MB) 7744MB (free=193MB)
    peak sys (opt OFF) 7395MB (free=547MB) ~7780MB (free=~160MB)

    Two separate regressions:

    1. optimizeChunks is now default-on and 8x more expensive in memory and time — the heap spikes from +20MB to +1627MB for the same chunk count.

    2. Even with NG_BUILD_OPTIMIZE_CHUNKS=false, ng22 uses ~350-400MB more system memory than ng21 during bundleAll. ng21 completes with ~547MB free; ng22 completes with ~160MB free — not enough for any subsequent process allocation.

    Our production app fails on ng22 even with chunk optimization explicitly disabled (NG_BUILD_OPTIMIZE_CHUNKS=false), while ng21 with chunk optimization fully enabled passes comfortably. The gap between them is ~350MB in the bundleAll phase alone.

    Workaround that partially helps: NG_BUILD_OPTIMIZE_CHUNKS=false removes the large spike and allows the Angular build itself to complete, but post-build steps still get OOM-killed due to the baseline regression.

    As the topic starter, I can't share the production app's code, but I'll be happy to run any measurements that may help debugging it.

  4. arturovt commented on Jun 17, 2026

    @arturovt
    Contributor

    Exactly the same issue we're facing that been described by @dmitry-stepanenko.

    Build OOM investigation results (supplier app, 32GB RAM, Node v24.15.0)

    # Command Bundler Chunk opt Peak JS heap Peak RSS Result Time
    1 NG_BUILD_PARALLEL_TS=false npx nx build supplier esbuild enabled (threshold=3) 4060 MB → OOM crash — ❌ Fatal: heap out of memory —
    2 NG_BUILD_OPTIMIZE_CHUNKS=0 npx nx build supplier esbuild disabled 342 MB 3869 MB ✅ Success 52s
    3 NG_BUILD_CHUNKS_ROLLDOWN=true npx nx build supplier rolldown enabled (threshold=3) 409 MB 4562 MB ✅ Success 54s

    Key observations:

    • The crash is caused by the chunk optimizer, not TypeScript compilation or bundling itself
    • The app produces 1160 lazy chunks (esbuild) / 1338 lazy chunks (rolldown) — with threshold=3, the optimizer evaluates a very large number of merge candidates
    • With esbuild, the JS heap grows to 4 GB+ during optimizeChunks; with rolldown the same pass completes at ~409 MB heap
    • Disabling chunk optimization (NG_BUILD_OPTIMIZE_CHUNKS=0) avoids the OOM entirely but leaves 1160 unmerged lazy chunks (initial bundle: 3.33 MB vs 1.21 MB with rolldown)
    • Rolldown also produces better-merged output (1.21 MB initial) suggesting its chunk graph representation is cheaper to optimize
  5. clydin commented on Jun 22, 2026

    @clydin
    Member

    @arturovt The bundler is always esbuild. The NG_BUILD_CHUNKS_ROLLDOWN option controls whether the post-build chunk optimizer stage uses either rollup (default for v22.0) or rolldown (upcoming default for v22.1 now that it is stable).

    Also, 22.0.2 contains a performance optimization (#33371) which should provide noticeable memory usage improvements outside of the chunk optimizer area. Please update to the latest patch to take advantage. Also @t-barak, if you have some updated numbers for your project with 22.0.2+ for comparison, that would be very helpful.

  6. dmitry-stepanenko commented on Jun 23, 2026

    @dmitry-stepanenko
    Contributor

    @clydin here's the comparison of ng21 with the latest, done on my machine for our 2 largest apps in the workspace. On Github Actions CI it OOMs, so it's not informative

    If it helps, provide me with patches for angular packages with logging you might be interested in.

    Local Build Memory Comparison

    Machine: macOS, 48 GB RAM
    Node options: --max_old_space_size=8192
    Config: app-1:build:production / app-2:build:production
    Flags: NX_SKIP_NX_CACHE=true NG_BUILD_OPTIMIZE_CHUNKS=false NG_BUILD_MAX_WORKERS=1
    Cache: .angular/cache and dist/ cleared before each run

    Label Angular @angular/build
    ng21 21.2.9 21.2.7
    ng22 22.0.2 22.0.3

    app-1

    Metric ng21 ng22 Δ
    bundleAll duration 69.5 s 74.9 s +8%
    NX total (yield) 76.8 s 81.9 s +7%
    RSS at bundleAll:done 6140 MB 6032 MB −2%
    Peak RSS (main) 5967 MB 6227 MB +4%
    Peak heapUsed (worker) 1697 MB 1674 MB −1%
    RSS at NX yield 1111 MB 6160 MB +454%
    System mem at yield 40355 / 49152 MB 48374 / 49152 MB —

    app-2

    Metric ng21 ng22 Δ
    bundleAll duration 51.5 s 57.8 s +12%
    NX total (yield) 56.9 s 63.0 s +11%
    RSS at bundleAll:done 4662 MB 5029 MB +8%
    Peak RSS (main) 4478 MB 5129 MB +15%
    Peak heapUsed (worker) 1356 MB 1340 MB −1%
    RSS at NX yield 1016 MB 5094 MB +401%
    System mem at yield 40667 / 49152 MB 46624 / 49152 MB —

    RSS at bundleAll:done vs NX yield

    app-1 (ng21):  bundleAll:done rss=6140 MB  →  NX yield rss=1111 MB
    app-1 (ng22):  bundleAll:done rss=6032 MB  →  NX yield rss=6160 MB
    
    app-2 (ng21):  bundleAll:done rss=4662 MB  →  NX yield rss=1016 MB
    app-2 (ng22):  bundleAll:done rss=5029 MB  →  NX yield rss=5094 MB
    

    What happens between bundleAll:done and yield

    I've added some logging inside esbuild's onDispose callback to track exactly what frees memory.

    ng21 — disposal happens before yield:

    bundleAll:done              rss=4837 MB
      compilation.close()     => rss=1606 MB   (freed ~3.2 GB)
      jsTransformer.close()   => rss=977 MB    (freed ~630 MB)
    NX yield                    rss=1018 MB
    

    ng22 — disposal happens after yield:

    bundleAll:done              rss=4061 MB
    NX yield                    rss=4043 MB   <= TS worker still alive
      onDispose fires here (post-yield)
    

    Below is AI analysis of everything I was able to extract:

    Root cause

    compilation.close() terminates the TypeScript compilation worker thread, which holds ~3 GB of the TypeScript program (ASTs, type checker, source file cache). In ng21 this cleanup was implicit — esbuild.build() (non-incremental) schedules onDispose via setTimeout(cb, 0) which fires before the async yield chain propagates up. Memory is freed before NX sees the result.

    In ng22, @angular/build introduced explicit result.dispose() in build-action.js (added in the new non-watch finally block). This is the right pattern for resource management, but the timing is wrong — it runs after yield* emitOutputResults() and after the NX executor receives and processes the result. The TS worker stays alive for the entire post-bundle phase and during file writing to disk.

    The delta between ng21 and ng22 at yield is ~3 GB, which is exactly the size of compilation.close() cleanup.


    Potential Fix

    In @angular/build's build-action.js, call result.dispose() (or at minimum compilation.close()) before yield* emitOutputResults() in non-watch mode. The disposal sequence needs to move from post-yield to pre-yield.

    This is an @angular/build issue — NX's executor just forwards yields and has no control over disposal timing.

  7. t-barak commented on Jun 23, 2026

    @t-barak
    Author

    I have patched build-action.js to apply this change, and it did not make a big difference (~0.3%).

  8. VeselyT commented on Jun 24, 2026

    @VeselyT

    We are experiencing the same issue. Since upgrading to Angular 22, we've had to switch to 32GB runners on AWS.

  9. wartab commented on Jun 30, 2026

    @wartab

    This is actually a show-stopper for us, we had to revert the update to Angular 22.
    CI systematically crashes and with RAM prices sky rocketing, migrating our self-hosted solution becomes an expensive endeavour.

  10. jpike88 commented on Jul 1, 2026

    @jpike88

    I have the same issue, even for our smaller projects RAM increase has been substantial. We've also had to bump up the size of our runners as well. ng serve is also sluggish. The angular team need to define tolerances for build performance.

  11. dmitry-stepanenko commented on Jul 10, 2026

    @dmitry-stepanenko
    Contributor

    @alan-agius4 @clydin any update on this?

  12. pumano commented on Jul 21, 2026

    @pumano

    Any news on this?

  13. 8 remaining items

  14. wartab commented on Jul 30, 2026

    @wartab

    If increasing memory of your machines is not feasable right now, you can consider running a custom node.js build with ./configure --experimental-enable-pointer-compression and halve node.js memory usage. See also blog.platformatic.dev/we-cut-nodejs-memory-in-half

    The catch: If any of your workloads need more than 8gb of RAM, this wont work for you, as that flag caps memory at 4gb per node.js worker (a 8gb heap becomes a 4gb heap with pointer-compression, hence the 4gb limit can be seen as a 8gb limit if you are measuring a "normal node.js" thread).

    It's not fully official yet. If you want to know why you can read nodejs/build#4352 and follow the breadcrumbs all the way back to 2020. But maybe if more people use it, it may gain traction.

    Of course if angular cli could use less memory that would be very welcome too. But using pointer-compression and fixing memory issues aren't mutually exclusive.

    I haven't tried, but pointer compression restricts the processes to use a maximum of 4GB. Builds that are affected by this issue will generally exceed this memory usage naturally.

  15. alan-agius4 commented on Jul 30, 2026

    @alan-agius4
    Collaborator

    Hey @Davidgimenez1997, that commit isn't part of the 22.1.0 release. I also couldn't track down a PR for it.

    Version 22.1.0 does come with a bunch of performance fixes, though could you give it a try and see if it resolves your issue?

  16. Davidgimenez1997 commented on Jul 31, 2026

    @Davidgimenez1997

    Good morning @alan-agius4 !

    After conducting several performance tests, it has been observed that Angular version 22.1.0 shows improvements, but it still doesn't reach the performance of the latest version, 21.

    Attached are graphs of a service that waits for pre-rendered build triggers (where an increase in resource consumption was detected after the update).

    The CPU and memory spikes occur because the service starts (performs a pre-rendered build) and immediately receives a build trigger, which is normal.

    Is it possible to control Node.js resource consumption during the Angular build process so that a pre-rendered build doesn't consume so many resources?

    Thank you very much for your answers!

    I appreciate your feedback.

    Image Image
  17. wartab commented on Jul 31, 2026

    @wartab

    I just attempted an upgrade to Angular 22.1 and CI still crashes, while it didn't in Angular 21:

    Killed
    fatal error: all goroutines are asleep - deadlock!
    
    goroutine 1 [chan receive]:
    github.com/evanw/esbuild/internal/helpers.(*ThreadSafeWaitGroup).Wait(...)
    	github.com/evanw/esbuild/internal/helpers/waitgroup.go:36
    main.runService.func2()
    	github.com/evanw/esbuild/cmd/esbuild/service.go:114 +0x53
    main.runService(0x1)
    	github.com/evanw/esbuild/cmd/esbuild/service.go:160 +0x637
    main.main()
    	github.com/evanw/esbuild/cmd/esbuild/main.go:252 +0xb16
    
    goroutine 22 [chan receive]:
    main.runService.func1()
    	github.com/evanw/esbuild/cmd/esbuild/service.go:98 +0x45
    created by main.runService in goroutine 1
    	github.com/evanw/esbuild/cmd/esbuild/service.go:97 +0x1ce
    
    goroutine 23 [chan receive]:
    main.(*serviceType).sendRequest(0xea5dc710ff0, {0xa10be0, 0xea60baa6ae0})
    	github.com/evanw/esbuild/cmd/esbuild/service.go:192 +0x12b
    main.runService.func3()
    	github.com/evanw/esbuild/cmd/esbuild/service.go:125 +0x3e
    created by main.runService in goroutine 1
    	github.com/evanw/esbuild/cmd/esbuild/service.go:122 +0x2c7
    Deleting duplicate background files.  []
    Failed to build Angular for brand falco-test with return code 137.
    

    And to further address the pointer compression suggestion, I doubt it would have any impact at all, given it's the Go process that seems to be OOMing.

  18. added 2 commits that reference this issue on Jul 31, 2026
    3709966
    902e8ea
  19. self-assigned this
    on Jul 31, 2026
  20. added theissue type on Jul 31, 2026
  21. added a commit that references this issue on Jul 31, 2026
    4fd8140
  22. added a commit that references this issue on Jul 31, 2026
    f4a0f84
  23. wartab commented on Aug 6, 2026

    @wartab

    @alan-agius4 Thanks for the new Angular CLI version, it did indeed resolve the issue! Thanks a lot!

  24. angular-automatic-lock-bot commented on Oct 18, 2026

    @angular-automatic-lock-bot

    This issue has been automatically locked due to inactivity.
    Please file a new issue if you are encountering a similar or related problem.

    Read more about our automatic conversation locking policy.

    This action has been performed automatically by a bot.

  25. locked and limited conversation to collaborators on Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions