fix(platform-browser): prevent memory leak of style nodes if shadow DOM encapsulation is used (#42005)

Prior to this change, any inserted `<style>` nodes into shadow dom trees would be retained
in memory, even after the shadow dom tree has been removed. This commit fixes the memory
leak by tracking the inserted `<style>` nodes per host element, such that removal of the
host element also releases the style nodes.

Fixes #36655

PR Close #42005
This commit is contained in:
JoostK 2021-05-08 20:29:06 +02:00 committed by Alex Rickabaugh
parent e071e3b507
commit 96624b71a7
5 changed files with 39 additions and 9 deletions

View File

@ -1430,6 +1430,9 @@
{ {
"name": "removeListItem" "name": "removeListItem"
}, },
{
"name": "removeStyle"
},
{ {
"name": "renderComponent" "name": "renderComponent"
}, },

View File

@ -1394,6 +1394,9 @@
{ {
"name": "removeListItem" "name": "removeListItem"
}, },
{
"name": "removeStyle"
},
{ {
"name": "renderComponent" "name": "renderComponent"
}, },

View File

@ -1832,6 +1832,9 @@
{ {
"name": "removeFromArray" "name": "removeFromArray"
}, },
{
"name": "removeStyle"
},
{ {
"name": "renderComponent" "name": "renderComponent"
}, },

View File

@ -34,35 +34,47 @@ export class SharedStylesHost {
@Injectable() @Injectable()
export class DomSharedStylesHost extends SharedStylesHost implements OnDestroy { export class DomSharedStylesHost extends SharedStylesHost implements OnDestroy {
private _hostNodes = new Set<Node>(); // Maps all registered host nodes to a list of style nodes that have been added to the host node.
private _styleNodes = new Set<Node>(); private _hostNodes = new Map<Node, Node[]>();
constructor(@Inject(DOCUMENT) private _doc: any) { constructor(@Inject(DOCUMENT) private _doc: any) {
super(); super();
this._hostNodes.add(_doc.head); this._hostNodes.set(_doc.head, []);
} }
private _addStylesToHost(styles: Set<string>, host: Node): void { private _addStylesToHost(styles: Set<string>, host: Node, styleNodes: Node[]): void {
styles.forEach((style: string) => { styles.forEach((style: string) => {
const styleEl = this._doc.createElement('style'); const styleEl = this._doc.createElement('style');
styleEl.textContent = style; styleEl.textContent = style;
this._styleNodes.add(host.appendChild(styleEl)); styleNodes.push(host.appendChild(styleEl));
}); });
} }
addHost(hostNode: Node): void { addHost(hostNode: Node): void {
this._addStylesToHost(this._stylesSet, hostNode); const styleNodes: Node[] = [];
this._hostNodes.add(hostNode); this._addStylesToHost(this._stylesSet, hostNode, styleNodes);
this._hostNodes.set(hostNode, styleNodes);
} }
removeHost(hostNode: Node): void { removeHost(hostNode: Node): void {
const styleNodes = this._hostNodes.get(hostNode);
if (styleNodes) {
styleNodes.forEach(removeStyle);
}
this._hostNodes.delete(hostNode); this._hostNodes.delete(hostNode);
} }
onStylesAdded(additions: Set<string>): void { onStylesAdded(additions: Set<string>): void {
this._hostNodes.forEach(hostNode => this._addStylesToHost(additions, hostNode)); this._hostNodes.forEach((styleNodes, hostNode) => {
this._addStylesToHost(additions, hostNode, styleNodes);
});
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this._styleNodes.forEach(styleNode => getDOM().remove(styleNode)); this._hostNodes.forEach(styleNodes => styleNodes.forEach(removeStyle));
} }
} }
function removeStyle(styleNode: Node): void {
getDOM().remove(styleNode);
}

View File

@ -47,6 +47,15 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
expect(doc.head).toHaveText('a {};b {};'); expect(doc.head).toHaveText('a {};b {};');
}); });
it('should remove style nodes when the host is removed', () => {
ssh.addStyles(['a {};']);
ssh.addHost(someHost);
expect(someHost.innerHTML).toEqual('<style>a {};</style>');
ssh.removeHost(someHost);
expect(someHost.innerHTML).toEqual('');
});
it('should remove style nodes on destroy', () => { it('should remove style nodes on destroy', () => {
ssh.addStyles(['a {};']); ssh.addStyles(['a {};']);
ssh.addHost(someHost); ssh.addHost(someHost);