Repository navigation
HostListener/output() memory leak in SSR #66506
Copy link
Copy link
Open
Labels
area: coreIssues related to the framework runtimeIssues related to the framework runtimecore: event dispatchmemory leakIssue related to a memory leakIssue related to a memory leak
Milestone
Description
Which @angular/* package(s) are the source of the bug?
Don't known / other
Is this a regression?
No
Description
It appears that elements are not being garbage collected in an SSR application if:
- A child component contains an
output()which it's parent is using - A component is using a
@HostListener
How to reproduce in provided minimal sample
- Either start the dev server or run a production build (both have the same result)
- Open the app
- Open dev tools
- Click the toggle button multiple times
- Take a snapshot of the heap
- You now see multiple detached instances of both components which are still held in memory even though not used anymore
Validation
- Remove
@HostListenerfromWithHostListenercomponent// Change this @HostListener('click', ['$event']) public onClick(event: MouseEvent): void {} // to this public onClick(event: MouseEvent): void {}
- Remove
WithOutputcomponent's output listener fromAppcomponent<!-- change this --> <app-with-output (myOutput)="doSomething()"></app-with-output> <!-- to this --> <app-with-output></app-with-output>
- Run the app
- Click the toggle button multiple times
- Take a heap snapshot
- Now you can see that the DOM Nodes are cleaned up correctly
Changing the WithHostListener component to cleaning up the click listener manually (as seen in the snippet below) also enables the GC to cleanup the app-with-host-listener nodes.
import { Component, ElementRef, inject, OnDestroy, Renderer2 } from '@angular/core';
@Component({
selector: 'app-with-host-listener',
template: `
<div>With HostListener</div>
`,
})
export class WithHostListener implements OnDestroy {
private readonly renderer = inject(Renderer2);
private cleanupListener: (() => void) | undefined;
private readonly el = inject(ElementRef);
constructor() {
this.cleanupListener = this.renderer.listen(this.el.nativeElement, 'click', event => {
this.onClick(event);
});
}
public onClick(event: MouseEvent): void {console.log(event)}
ngOnDestroy(): void {
if (this.cleanupListener) {
this.cleanupListener();
}
}
}Please provide a link to a minimal reproduction of the bug
https://github.com/mii-shell/angular-dom-nodes-memory-leak
Please provide the exception or error you saw
No exception/error was thrown
Please provide the environment you discovered this bug in (run ng version)
Angular CLI : 21.0.5
Angular : 21.1.0-rc.0
Node.js : 24.4.1
Package Manager : npm 11.4.2
Operating System : linux x64
┌───────────────────────────┬───────────────────┬───────────────────┐
│ Package │ Installed Version │ Requested Version │
├───────────────────────────┼───────────────────┼───────────────────┤
│ @angular/build │ 21.1.0-rc.0 │ ^21.1.0-rc.0 │
│ @angular/cli │ 21.0.5 │ ^21.0.5 │
│ @angular/common │ 21.1.0-rc.0 │ ^21.1.0-rc.0 │
│ @angular/compiler │ 21.1.0-rc.0 │ ^21.1.0-rc.0 │
│ @angular/compiler-cli │ 21.1.0-rc.0 │ ^21.1.0-rc.0 │
│ @angular/core │ 21.1.0-rc.0 │ ^21.1.0-rc.0 │
│ @angular/forms │ 21.1.0-rc.0 │ ^21.1.0-rc.0 │
│ @angular/platform-browser │ 21.1.0-rc.0 │ ^21.1.0-rc.0 │
│ @angular/platform-server │ 21.1.0-rc.0 │ ^21.1.0-rc.0 │
│ @angular/router │ 21.1.0-rc.0 │ ^21.1.0-rc.0 │
│ @angular/ssr │ 21.1.0-rc.0 │ ^21.1.0-rc.0 │
│ rxjs │ 7.8.2 │ ~7.8.0 │
│ typescript │ 5.9.3 │ ~5.9.2 │
│ vitest │ 4.0.17 │ ^4.0.8 │
└───────────────────────────┴───────────────────┴───────────────────┘
Anything else?
- I originally thought this problem might be related to Event replay memory leak #59261, which is why I've used Angular version
21.1.0-rc.0in my minimal reproduction (which is the version in which this was fixed according to the changelog). Unfortunately, it did not resolve the issue. - I've also tested it with version 21.0.5, which resulted in the same
- Manual garbage collection does not remove the elements from the heap either
- Identical issue, regardless of whether
@HostListenerorhostoption in@Componentis used - This issue does not occur in a basic Angular app setup without SSR
Reacted by Twenty, David Roth, Shuaib Hasan Akib, Jaime Burgos, stefangrasboeck, sleepwalkerffs, Mathias Draxler, Michael Zangerle, Dominic Luidold, Arjan Frans and 4 more
Metadata
Metadata
Assignees
Labels
area: coreIssues related to the framework runtimeIssues related to the framework runtimecore: event dispatchmemory leakIssue related to a memory leakIssue related to a memory leak
Activity
JeanMeche commentedon Jan 13, 2026
I can confirm this is directly related to event replay. Removing it resolved the leak.
fix(core): prevent event replay from retaining elements with listeners
JeanMeche commentedon Jan 15, 2026
fwiw, this is where the leak comes from:
angular/packages/core/src/hydration/event_replay.ts
Lines 112 to 122 in 417c385
Elements with listeners are retained by this callback until cleanup.
fix(core): prevent event replay from retaining elements with listeners
fix(core): prevent event replay from retaining elements with listeners
fix(core): prevent event replay from retaining components with elemen…
fix(core): prevent event replay from retaining components with elemen…
fix(core): prevent event replay from retaining components with elemen…
fix(core): prevent event replay from retaining components with elemen…
fix(core): prevent event replay from retaining components with elemen…
fix(core): prevent event replay from retaining components with elemen…
fix(core): prevent event replay from retaining components with elemen…
fix(core): prevent event replay from retaining components with elemen…
fix(core): prevent event replay from retaining components with elemen…
mii-shell commentedon Aug 7, 2026
@JeanMeche Thanks for investigating and providing a PR with a fix!
Is there any news on when the fix will make it into a release?
Due to this bug, we had to remove event replay for now. It would be great if we could enable it again any time soon 🙏
thekhegay commentedon Oct 23, 2026
hit this on docs site of my ui lib - every visited route stays in memory. still reproducible on 22.2.3, and in v22 it got wider:
provideClientHydration()now turns on incremental hydration by default and that bringswithEventReplay()with it. so defaultng new --ssrapp leaks, onlywithNoIncrementalHydration()avoids it. plain template listeners leak too, not only@HostListener/output()prod build, prerendered, 200
<button (click)>per route, client side navigation between two routes, forced GC:withEventReplay()(or v22 default)withNoIncrementalHydration()retainer path from heap snapshot:
stashEventListeners(module Map) - stash callback thatwithEventReplay()registers per APP_ID - its capturedjsActionMap-''block Set - detached element.''set is deleted once at stability, evrythinglistenToDomEventstashes after that stays untilappRef.onDestroy#66594 looks like right fix, is anything blocking it?
radomirchev commentedon Oct 24, 2026
Another data point, from a data grid inside one page (not route navigation). Data grids re-create their rows and cells while the user scrolls, filters or resizes, so every element that gets a template listener after hydration stays in the replay stash and the page keeps growing without any navigation.
Prerendered page with an Ignite UI for Angular grid of about 50 rows,
@angular/core22.1.4, production build, Chrome, 12 narrow/wide re-layout cycles followed by 12 filter/clear cycles, garbage collection forced before each reading:withEventReplay()The retained elements are the grid's rows, cells and chips (110 of 110 checked ones carried the replay stash; none without event replay).
I compared the code in
@angular/core22.2.3 and 22.3.0-next.2 (compared, not run): the stash callback, the one-timejsActionMap.delete('')and thelistenToDomEventstash call are unchanged, and #66594 is still open.fix(core): prevent event replay from retaining components with elemen…