angular: __ngContext__ magnifies native Chrome memory leaks

🐞 bug report

Affected Package

@angular/core@11.2.3

Is this a regression?

No.

Description

Small memory leaks become large leaks when Angular monkey patches __ngContext__ onto dom.

For example, take this issue. After typing into textareas, the issue leads to detached HTMLTextAreaElements because of the native undo stack.

It is a small leak that has nothing to do with Angular. It is only a few KBs:

image

But, with Angular components in the mix, those KBs become MBs:

image

It looks like __ngContext__, retains almost everything, even though the textarea is a vanilla element:

image

And if we remove this call from @angular/core:

image

🔬 Minimal Reproduction

https://github.com/blidblid/ng-context-memory-leak

  1. ng s --prod
  2. Type in the textarea to create the Chrome leak
  3. Press Destroy and create
  4. Repeat with @angular/material components included

The issue is also present on material.angular.io. Typing in a textarea and navigating the sidenav will cause large leaks.

🌍 Your Environment

Angular Version:


Angular CLI: 11.2.2
Node: 14.15.0
OS: win32 x64

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

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1100.7
@angular-devkit/build-angular   0.1100.7
@angular-devkit/core            11.0.7
@angular-devkit/schematics      11.2.2
@angular/cdk                    11.2.2
@angular/cli                    11.2.2
@angular/material               11.2.2
@schematics/angular             11.2.2
@schematics/update              0.1102.2
rxjs                            6.6.6
typescript                      4.0.7

Anything else relevant?

Chrome 88.0.4324.190

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 31
  • Comments: 29 (15 by maintainers)

Commits related to this issue

Most upvoted comments

We discussed this some more and think we might be able to use requestIdleCallback to directly clear __ngContext__ (and mitigate DOM memory leaks outside our control) without incurring runtime performance penalty. We’d definitely have to try this out with a number of different apps, though, to see the real-world impact.

@mhevery Does this also apply to patching native elements specifically?

From our perspective, this issue would put 100.000 healthcare workers on ViewEngine indefinitely. That’s a huge blow, because Ivy is amazing and because we’ll be stranded with an old Angular version. The background here is that the hospital environments usually have 100-200 MB of RAM. And if the framework magnifies leaks by a factor thousand, those environments will go down.

I get that the reproduction above uses a Chrome issue, but the role of __ngContext__ as a massive retainer applies to any leak. Angular has 13 open memory leaks. We have some, rxjs has some, Chrome has some. Any large app will leak a bit, and that’s fine as long as those leaks are small and self-contained. __ngContext__ removes that containment.

I’m happy to rephrase the issue to “ngContext magnifies memory leaks” if you think that is a better angle here.

For anybody looking for an ugly workaround, I’ve yet to see any issues with this patch of attachPatchData:

import { process as runNgcc } from '@angular/compiler-cli/ngcc';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path';

const ANGULAR_CORE_PATH = '@angular/core';
const ANGULAR_CORE_NGCC_PATH = `${ANGULAR_CORE_PATH}/__ivy_ngcc__/fesm2015/core.js`;

const REPLACE_AT = `
function attachPatchData(target, data) {
    ngDevMode && assertDefined(target, 'Target expected');
    target[MONKEY_PATCH_KEY_NAME] = data;
}
`.trim();

const REPLACE_WITH = `
function attachPatchData(target, data) {
    ngDevMode && assertDefined(target, 'Target expected');
    if (ngDevMode || !(target instanceof EventTarget)) {
      target[MONKEY_PATCH_KEY_NAME] = data;
    }
}
`.trim();

// https://github.com/angular/angular/issues/41047
export function patchAngularToRemoveNgContextFromDom(nodeModulesPath: string): void {
  const patchFilePath = join(nodeModulesPath, ANGULAR_CORE_NGCC_PATH);

  if (!existsSync(patchFilePath)) {
    runNgcc({
      basePath: nodeModulesPath,
      targetEntryPointPath: join(nodeModulesPath, ANGULAR_CORE_PATH),
      createNewEntryPointFormats: true,
      propertiesToConsider: ['fesm2015'],
    });

    if (!existsSync(patchFilePath)) {
      throw new Error(`Tried to patch @angular/core but could not find ${patchFilePath}.`);
    }
  }

  const fileContent = readFileSync(patchFilePath, 'utf8');
  const replacedFileContent = fileContent.replace(REPLACE_AT, REPLACE_WITH);

  if (!replacedFileContent.includes(REPLACE_WITH)) {
    throw new Error(`Could not patch memory leak in @angular/core.`);
  }

  if (fileContent.length === replacedFileContent.length) {
    return;
  }

  writeFileSync(patchFilePath, replacedFileContent);
}

You are a life saver! With a similar fix our app is now smooth and performant on even the slower iOs Air2s ( ours is a hybrid ionic Ng app ). Basically we have 100s of cards that can be paginated back and forth and the dom nodes and memory were climbing up. We added this “hack” and the UI is now much better than ever. Our diffs for the patch were

6296a6297
>     // testing FESM
6298c6299,6301
<     target[MONKEY_PATCH_KEY_NAME] = data;
---
>     if (ngDevMode || !(target instanceof EventTarget)) {
>       target[MONKEY_PATCH_KEY_NAME] = data;
>     }
7389a7393
>                             delete callContext[MONKEY_PATCH_KEY_NAME];delete callContext._ref;
7399a7404
>                         delete context[MONKEY_PATCH_KEY_NAME];delete context._ref;
7691a7697
>         delete rNode[MONKEY_PATCH_KEY_NAME];

and our patch , in npm post-install is to call this after the ngcc has executed:-

patch -N node_modules/@angular/core/bundles/core.umd.js patches/@angular+core+12.2.7.umd.patch || true
patch -N node_modules/@angular/core/fesm2015/core.js patches/@angular+core+12.2.7.fesm2015.patch || true

The downside is we have to keep track when we update Angular

Update on my previous comment: it seems like I overlooked something in my previous fix which can cause some different memory leaks. I’ve submitted https://github.com/angular/angular/pull/41894 to resolve them and I expect the fix to be in the RC release next week.

We’ve merged in a fix for this which will be out in the next RC version. It would be valuable to know if the changes resolved the memory leaks for you.

Besides that I agree with @kallebjork I’m also curious about what APIs need this in production and why.