adoptedStyleSheets会导致内存泄漏

前端开发 2026-07-08

描述

我在处理自定义元素,并且通过使用 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]; 行注释掉并重新运行代码,内存标签页将不会显示这些节点(大概是因为它们已被正确垃圾回收)。

10 个与已移除元素对应的分离节点

保留路径

已添加一个示例中某个元素的保留路径图像。 在此输入图片描述

浏览器

我再次测试了最小可复现的代码,似乎在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。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章