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": "removeStyle"
},
{
"name": "renderComponent"
},

View File

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

View File

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

View File

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