adoptedStyleSheets会导致内存泄漏
描述
我在处理自定义元素,并且通过使用 CSSStyleSheet() 和 adoptedStyleSheets,为影子元素准备了一个共享的样式表。我的自定义元素在某些时刻可能会被手动从DOM中移除(按设计),但我发现我一直存在内存泄漏,我设法将其直接与 adoptedStyleSheets 联系起来。当我在Chrome浏览器的开发者工具中查看“Detached elements”(打开开发者工具 > 内存标签页 > 选择“Detached elements” > 点击“Take snapshot”),我可以看到从DOM中移除后,自定义元素仍然被阻止垃圾回收。但是,我不知道为什么会这样,也不明白为什么样式表会指向我的元素(尤其是在采用的样式表被手动移除 adoptedStyleSheets = [] 之后)。我是不是做错了?
我已尝试的办法
- 如果我根本不设置
shadow.adoptedStyleSheets,这种奇怪的行为就不会发生——元素会像正常一样被垃圾回收。但这显然不是解决方案,因为我想使用采用的样式表来为我的组件着色(页面上可能同时存在数百个组件)。 - 如果我保留
shadow.adoptedStyleSheets = [stylesheet],并随后用shadow.adoptedStyleSheets = []取消链接该样式表,尽管样式已经在视觉上被移除,行为仍然会发生。 - 在调用
super.remove()之后,覆盖remove()方法并尝试执行诸如this.shadowRoot?.firstElementChild?.remove()、this.innerHTML = ''等怪异操作,也不能阻止这种行为。 - 在构造函数的作用域内定义
stylesheet也不能防止这种奇怪的行为。
最小可复现代码
我提供了一段最小可复现的JavaScript。它创建一个样式表,将其添加到自定义元素的 adoptedStyleSheets,然后对该自定义元素进行追加和移除共10次。
"use strict";
// CREATE STYLE SHEET
const sheetString = `.hello { width: 100px; height: 100px; background-color: #bbffff; border: 1px solid black; padding: 5px; }`
const stylesheet = new CSSStyleSheet();
stylesheet.replaceSync(sheetString);
// CUSTOM ELEMENT CLASS
class MyElement extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({mode: 'open'});
// ADOPT STYLE SHEET
shadow.adoptedStyleSheets = [stylesheet];
// shadow.adoptedStyleSheets = [];
const htmlString = `
<div class="hello">HELLO!</div>
`;
const template = document.createElement('template');
template.innerHTML = htmlString;
const element = template.content.cloneNode(true);
shadow.append(element);
}
}
customElements.define('my-element', MyElement);
// ADD AND IMMEDIATELY REMOVE 10 TIMES
for (let i = 0; i < 10; ++i) {
document.documentElement.querySelector('body')?.appendChild(new MyElement());
document.querySelector('my-element')?.remove();
}
我用Parcel版本2.9.0和 Live Server(均在本地通过VS Code运行)运行过它,并在Firefox Developer Edition和 Chrome中尝试过(编辑:问题似乎只在Chrome和 Edge中出现,在Firefox中未出现。见下方“浏览器”部分)。如附在Chrome的内存标签页中的图片所示,确实有10个已分离的节点以某种方式仍然占用空间。如果将第 shadow.adoptedStyleSheets = [stylesheet]; 行注释掉并重新运行代码,内存标签页将不会显示这些节点(大概是因为它们已被正确垃圾回收)。

保留路径
浏览器
我再次测试了最小可复现的代码,似乎在Firefox上根本不是问题。把迭代次数从10增加到1000再到10000时,如果在拍快照前多等几秒钟,Firefox的快照总是会回到大约0.50 MB。以前,我猜是在Firefox有机会进行垃圾回收之前就拍了快照。
然而,在Chrome中,随着迭代次数的增加,“Total JS heap size” 也在增加(0次迭代时堆大小约为5MB。达到10000次迭代时,卡在50MB以上)。我也在Edge中进行了测试,结果完全相同。值得注意的是,Edge的开发者工具中的“Memory”区域看起来与Chrome相同,甚至还有实际查看已分离元素的选项(与Firefox不同)。与Chrome一样,额外的卡顿内存来自被某些东西引用的自定义元素。
解决方案
这是由这个变更集引起的:https://chromium.googlesource.com/chromium/src/+/23a0a9f24481cba233e5d4b2c8a5e95d666f533a
它其实是开发者工具的一部分,因此如果你在关闭开发者工具的情况下加载页面(就像大多数用户可能会这样做),就不会发生内存泄漏。但(很可能)你也无能为力。
尽管如此,在节点被移除后仍然保留引用也许并非人们所期望。因此,最好在https://issues.chromium.org/issues/wizard提出一个问题。
你已经提供了足够的信息,足以让triage团队复现,但你现在甚至可以把组件设置为 Platform>DevTools,并让他们知道,我们通过二分排序找到了罪魁祸首就是上述的CL。
