惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
Engineering at Meta
Engineering at Meta
月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tailwind CSS Blog
博客园 - Franky
The GitHub Blog
The GitHub Blog
大猫的无限游戏
大猫的无限游戏
The Cloudflare Blog
B
Blog RSS Feed
云风的 BLOG
云风的 BLOG
小众软件
小众软件
罗磊的独立博客
Microsoft Azure Blog
Microsoft Azure Blog
I
InfoQ
美团技术团队
H
Hackread – Cybersecurity News, Data Breaches, AI and More
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
V2EX
C
Check Point Blog
WordPress大学
WordPress大学
博客园 - 【当耐特】
博客园 - 司徒正美
D
Docker

Jake Archibald's blog

Controlling when CSS custom properties are computed Fixing my tooltip accessibility mistake The Goldilocks customizable select height Importing vs fetching JSON The present and potential future of progressive image rendering Fetch streams are great, but not for measuring upload/download progress Making XML human-readable without XSLT Give footnotes the boot Animating zooming using CSS: transform order is important… sometimes How should <​selectedoption​> work? Video with alpha transparency on the web Garbage collection and closures
Firefox + custom elements + iframes bug
2025-02-14 · via Jake Archibald's blog

Over at Shopify we've been building a bunch of web components to use internally and in third party contexts. All of a sudden, we found some strange errors in our logs, all from Firefox. This is the post I wish existed when we discovered it.

Update: This is now fixed, and should land in Firefox 151.

The bug

The bug happens when a custom element (or web component) is moved to a document from another JavaScript Realm [spooky noises]. A Realm is a separate JavaScript context with its own global, own implementation of Array etc etc. An iframe or popup window provides a document in a new JavaScript Realm.

The result of the bug is that the element's custom prototype is lost, and things like instance methods disappear.

Ok ok, sorry, I'm trying to hit all the terms that people might search for to find a fix for this issue. Anyway, here's a simple custom element:

class MyElement extends HTMLElement {
  say(message) {
    console.log(message);
  }
  connectedCallback() {
    this.say('hello!');
  }
  disconnectedCallback() {
    this.say('goodbye!');
  }
}

customElements.define('my-element', MyElement);

And I'm going to create an instance of the element, and put it in an iframe:

// Create the iframe
const iframe = document.createElement('iframe');
document.body.append(iframe);

// Create the element, and put it in the iframe
const myElement = document.createElement('my-element');
iframe.contentDocument.body.append(myElement);

This fails in Firefox with "this.say is not a function", within connectedCallback. In fact, Firefox has lost all of the instance methods of the custom element. It's 'downgraded' to an instance of HTMLElement in the iframe Realm, rather than MyElement.

It's kinda funny, because the error happens within connectedCallback, which is an instance method, but even that instance method has gone. I assume the calling of connectedCallback was queued up before the prototype was lost.

If I put the element in the main document before moving it to the iframe, it fails in disconnectedCallback for the same reason.

Unfortunately this has been a known issue for 6 years.

The fix

The fix is kinda simple. Just… put the prototype back. As in, make it an instance of MyElement again.

class CustomElementBase extends HTMLElement {
  // This is called when the element is moved to a new document.
  // This is where we solve the bug if the element is moved to an iframe
  // without first being put into the main document.
  adoptedCallback() {
    rescueElementPrototype(this);
  }

  // This is called when the element is disconnected from a document.
  // This happens whenever the element is moved around the DOM,
  // but it also happens when the element is moved to a new document.
  // This happens before adoptedCallback,
  // so we need to fix it here,
  // to avoid the bug in subclass disconnectedCallback calls.
  disconnectedCallback() {
    rescueElementPrototype(this);
  }
}

function rescueElementPrototype(element) {
  // Return if everything looks as expected.
  if (element instanceof CustomElementBase) return;

  // Otherwise, get the intended constructor…
  const constructor = customElements.get(element.tagName.toLowerCase());

  // …and set the prototype.
  Object.setPrototypeOf(element, constructor.prototype);
}

Thanks to my colleague Anthony Frehner who realised customElements.get is a simple way to get the original constructor back, rather than the mad WeakMap hack I was using.

Now, make sure your custom elements extend this base class, and ensure you call super methods:

class MyElement extends CustomElementBase {
  say(message) {
    console.log(message);
  }
  connectedCallback() {
    super.connectedCallback?.();
    this.say('hello!');
  }
  disconnectedCallback() {
    super.disconnectedCallback?.();
    this.say('goodbye!');
  }
}

And that's it! The bug is undone, and everything works as expected.

View this page on GitHub