Skip to content

Conversation

@arturovt
Copy link
Contributor

@arturovt arturovt commented Nov 6, 2025

This issue has been captured in real-world scenarios multiple times,
particularly in SSR environments and micro-frontend applications where
memory leaks were observed after repeated application bootstrapping
and destruction cycles.

Previously, the freeConsumers array was a module-level singleton
that accumulated consumers across multiple application lifecycles.
This caused memory leaks in the following scenarios:

  • Multiple SSR requests in the same Node.js process
  • Multiple micro-frontend app instances in the same browser context
  • Application restarts during development (HMR)

Without proper scoping, each destroyed application left its pooled
consumers in memory, leading to unbounded growth over time.

This change replaces the global array with a WeakMap keyed by
ApplicationRef. ApplicationRef is used instead of EnvironmentInjector
because there is only one ApplicationRef per application, whereas there
are many EnvironmentInjector instances (per route, defer block, etc.).
This ensures:

  • Each application instance gets its own consumer pool
  • All views within an application share the same pool for efficient
    memory reuse
  • Pools are automatically garbage collected when the application
    is destroyed (when the ApplicationRef is GC'd)
  • Multiple concurrent applications don't interfere with each other's
    consumer recycling
  • No manual cleanup is required in ApplicationRef

The implementation uses optional injection for ApplicationRef to maintain
backward compatibility with existing tests (both OSS and G3) that create
components with custom injectors. In edge cases where ApplicationRef is
not available, consumers are created/discarded without pooling.

While it is barely possible to create a unit test that reliably
reproduces this multi-application scenario (due to module-level state
and the need to simulate multiple app lifecycles in the same process),
the WeakMap approach is fundamentally safer than the previous global
singleton and the fix is necessary without considerations.

Fixes memory leaks in production SSR deployments and micro-frontend
architectures.

@angular-robot angular-robot bot added the area: core Issues related to the framework runtime label Nov 6, 2025
@ngbot ngbot bot added this to the Backlog milestone Nov 6, 2025
This issue has been captured in real-world scenarios multiple times,
particularly in SSR environments and micro-frontend applications where
memory leaks were observed after repeated application bootstrapping
and destruction cycles.

Previously, the `freeConsumers` array was a module-level singleton
that accumulated consumers across multiple application lifecycles.
This caused memory leaks in the following scenarios:

* Multiple SSR requests in the same Node.js process
* Multiple micro-frontend app instances in the same browser context
* Application restarts during development (HMR)

Without proper scoping, each destroyed application left its pooled
consumers in memory, leading to unbounded growth over time.

This change replaces the global array with a `WeakMap` keyed by
`ApplicationRef`. `ApplicationRef` is used instead of `EnvironmentInjector`
because there is only one `ApplicationRef` per application, whereas there
are many `EnvironmentInjector` instances (per route, defer block, etc.).
This ensures:

* Each application instance gets its own consumer pool
* All views within an application share the same pool for efficient
  memory reuse
* Pools are automatically garbage collected when the application
  is destroyed (when the `ApplicationRef` is GC'd)
* Multiple concurrent applications don't interfere with each other's
  consumer recycling
* No manual cleanup is required in `ApplicationRef`

The implementation uses optional injection for `ApplicationRef` to maintain
backward compatibility with existing tests (both OSS and G3) that create
components with custom injectors. In edge cases where `ApplicationRef` is
not available, consumers are created/discarded without pooling.

While it is barely possible to create a unit test that reliably
reproduces this multi-application scenario (due to module-level state
and the need to simulate multiple app lifecycles in the same process),
the `WeakMap` approach is fundamentally safer than the previous global
singleton and the fix is necessary without considerations.

Fixes memory leaks in production SSR deployments and micro-frontend
architectures.
@arturovt arturovt force-pushed the fix/core_consumer_pool branch from 54a387f to dba3163 Compare November 7, 2025 01:05
@arturovt arturovt changed the title fix(core): scope reactive consumer pool per EnvironmentInjector fix(core): scope reactive consumer pool per ApplicationRef Nov 7, 2025
const consumer = freeConsumers.pop() ?? Object.create(REACTIVE_LVIEW_CONSUMER_NODE);
// Use optional injection to avoid breaking existing tests (both OSS and G3) that create
// components with custom injectors that may not provide `ApplicationRef`.
const appRef = lView[INJECTOR].get(ApplicationRef, null, {optional: true});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using DI in the hot path of change detection is something we try to avoid. You can key the map off of lView[ENVIRONMENT].changeDetectionScheduler instead of ApplicationRef

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that sounds better.

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

Labels

area: core Issues related to the framework runtime

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants