angular: Components not being garbage collected by browser, causing major memory leak

Which @angular/* package(s) are the source of the bug?

Don’t know / other

Is this a regression?

No

Description

Stack Overflow Post: https://stackoverflow.com/questions/71026959/memory-leak-garbage-collection-in-browser-not-collecting-components

Summary:

  • when a component gets created with *ngIf=“true” or with ViewContainerRef.createComponent(componentFactory: ComponentFactory<C>), and then destroyed, with *ngIf=“false” or with ViewContainerRef.clear(), the component stays in memory, and can’t be garbage collected by the browser
  • this causes a significant memory leak (especially in larger apps that use many nested components)
  • this is a tricky bug because:
    • if you download the minimal reproduction app, either a or b will happen a. it will leak memory right away, and components won’t be garbage collected (regularly or with the manual garbage collection button provided by most browsers for debugging purposes) b. it won’t leak memory right away, and it will happily garbage collect components regularly or with the manual garbage collection button provided by most browsers for debugging. After enough time has elapsed, or after you’ve restarted the app an arbitrary amount of times, it will eventually get into the memory leaking state
    • therefore, if my coworkers and I download the repo, then run the app, it seems to be a coin toss on whether or not this bug gets reproduced right away
    • if you’re one of the lucky few that doesn’t get it to happen right away, perhaps try it on a different computer, or wait a couple minutes and refresh the web app a couple times
  • the fact that it doesn’t happen first try every time makes it hard to prove that there is a bug with this minimal reproduction app
    • I’ve included screen shots in the stack overflow post (near the bottom), that proves that this is indeed happening
    • it does however happen every time with a much larger production app that I’m working on

Please provide a link to a minimal reproduction of the bug

Repo: https://github.com/kevinpbaker/angular-memory-killer S3 bucketed app: https://angular-memory-killer.s3.us-west-2.amazonaws.com/index.html

Please provide the exception or error you saw

There is no error thrown. The components in memory aren't being garbage collected, and will eventually crash the browser window.

Please provide the environment you discovered this bug in (run ng version)

I have upgraded this project to be the latest version of angular 12, and the latest version of angular 13, and it leaks in 11, 12, and 13. I have also tried it on multiple computers (M1 mac, older intel mac, windows pc, windows laptop).

Angular CLI: 11.2.18
Node: 16.14.0
OS: darwin arm64

Angular: 11.2.14
... animations, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router
Ivy Workspace: Yes

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1102.18
@angular-devkit/build-angular   0.1102.18
@angular-devkit/core            11.2.18
@angular-devkit/schematics      11.2.18
@angular/cdk                    11.2.13
@angular/cli                    11.2.18
@angular/material               11.2.13
@schematics/angular             11.2.18
@schematics/update              0.1102.18
rxjs                            6.5.5
typescript                      4.0.8

Anything else?

I’ve included a link to a stack overflow post I made that adds additional information and pictures to show that it is indeed leaking memory.

Microsoft Edge has an experimental feature called “Detached Elements” that can be enabled to see any objects that are dangling and ready to be garbage collected. You can use this feature to visually see that the angular components aren’t being garbage collected (I’ve also included screen shots of this in the stack overflow post).

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 10
  • Comments: 21 (10 by maintainers)

Most upvoted comments

@kevinpbaker thanks for the update!

Chromium bug: https://bugs.chromium.org/p/chromium/issues/detail?id=1308845

This issue has been fixed within the chromium browser. It hasn’t been released yet, but it can be tested with Chrome’s Canary browser: https://www.google.com/intl/en_ca/chrome/canary/

Broken Version 100.0.4896.60 (Official Build) (arm64)

https://user-images.githubusercontent.com/48214072/162014156-d33d5521-a5fc-4554-ade2-c7a943923a16.mp4

Fixed Version 102.0.4987.0 (Official Build) canary (arm64)

https://user-images.githubusercontent.com/48214072/162014327-751da40f-35fd-429a-8a1c-9fcc32225b43.mp4

Interesting, #41047 does mention that the leaks were specific to Chrome. It may be worth checking again after next week’s 14.0.0-next release which contains the __ngContext__ fix. You can also try it now by installing from https://github.com/angular/core-builds.

@crisbeto after a lot more testing, I think this issue is specific to Microsoft Edge and Chrome. I was unable to get Safari or Firefox to leak memory using the memory kill app provided.

Safari

The memory footprint in Safari will always climb and then plateau. Even if I spam the garbage collection button it remains at or below the plateau value. Here is an image showing this behavior: Screen Shot 2022-02-18 at 10 52 12 AM

Firefox

The memory footprint in Firefox gets cleaned up regardless of how many times I try to get it into the leaking memory state. Here is an image showing this behavior: Screen Shot 2022-02-18 at 10 45 52 AM


While doing this I had all 4 browsers running at the same time. All of them running the memory killer app. Microsoft Edge and Chrome would leak memory, but Safari and Firefox would not.

From the tests and information above, I think we can conclude that this isn’t an Angular Framework problem, but a browser problem.

Also, one of the computers is factory new that I used to leak memory on Microsoft Edge and Chrome. I install node.js, then I clone the memory killer app using git clone https://github.com/kevinpbaker/angular-memory-killer.git, then I run npm install and ng serve --prod --port 4204. I am able to get Microsoft Edge and Chrome to leak memory doing this.

To answer the questions:

  1. The solution I provided above is a quick way to check if the __ngContext__ fix would resolve the issue in the memory killer app, but it isn’t foolproof. The actual solution is in #41047.
  2. __ngContext__ doesn’t cause memory leaks by itself. If the app doesn’t have leaks at all, __ngContext__ isn’t going to introduce new leaks. It comes into play if the apps has some leaks which __ngContext__ will make worse.
  3. Style attributes won’t be affected by the __ngContext__ issue. We have one code path where a reference to the host DOM node will be retained when there are styles associated with it. It only applies to components with encapsulation: ViewEncapsulation.ShadowDom though.

As for reproducing it, it’s entirely possible that I’m not doing something in the same way as you or there’s something in my environment that prevents it from happening. What I did today was to npm i, ng serve and then take memory snapshots in the dev tools. I had it running for about 15min with no change in the amount memory being consumed.

One thing worth trying is to check if the leak happens in other browsers like Firefox or Safari. It would at least help us narrow it down to a framework issue or a potential problem with a browser.

I tried running both the memory killer app and the menu app mentioned above, but I couldn’t get it to leak more than 13 DOM nodes which would usually be GCed on the next snapshot. There’s a recording of my timeline below.

https://user-images.githubusercontent.com/4450522/154483715-0773ccc2-4a64-4bf1-b02d-0e817e980513.mov

Looking at your heap dump, it does have some references in __ngContext__ so it’s possible that https://github.com/angular/angular/pull/45051 will help with it. It’s hard to say before the change is actually released though. You could try changing the MemoryKillerComponent to the following which should have a similar effect:

export class MemoryKillerComponent {
  constructor(private _elementRef: ElementRef) {}

  ngOnDestroy() {
    [
      this._elementRef.nativeElement,
      ...this._elementRef.nativeElement.querySelectorAll('*'),
    ].forEach((el) => {
      el.__ngContext__ = null;
    });
  }
}