Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MutationObserver flag to observe mutations in (open) shadow trees to facilitate polyfills of HTML syntax #1287

Open
LeaVerou opened this issue May 8, 2024 · 13 comments
Labels
addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest topic: shadow Relates to shadow trees (as defined in DOM)

Comments

@LeaVerou
Copy link

LeaVerou commented May 8, 2024

What problem are you trying to solve?

It is currently exceedingly difficult (and slow) to implement polyfills of certain features that affect HTML syntax across both light and shadow DOM because it requires observing mutations in shadow trees as well, which is currently incredibly difficult, and all solutions have prohibitive performance.

Some examples:

  • Any of the custom attributes proposals (example)
  • Any new HTML element or attribute

What solutions exist today?

The current options are:

  1. 🤷🏽‍♀️ The shrug method: Use a regular document-wide mutation observer and simply accept that the polyfill will not work correctly when shadow trees are involved
  2. 🩹 The monkey patch method: Wrap HTMLElement.prototype.attachShadow() with code that calls mutationObserver.observe() on it, like so:
let originalAttachShadow = HTMLElement.prototype.attachShadow;
HTMLElement.prototype.attachShadow = function(...args) {
	let node = originalAttachShadow.call(this, ...args);
	mutationObserver.observe(node, options);
	return node;
}
  1. 🤕 The painful method: When the mutation observer detects new elements, check if they have a shadowRoot and call observe() on that too. Loop over all existing elements and attach mutation observers to the shadow roots of those with one.

As you can see, all of these are pretty terrible. They are slow, obtrusive, and wasteful. Can we do better?

How would you solve it?

Option 1

This is the most straightforward, lowest effort option: Simply add a new option to MutationObserverInit to observe (open) shadow trees. Name TBB, some ideas might be shadow, shadowRoots.

The MVP could be a boolean option, which could later be extended to more granularity (e.g. how many levels deep? What types of shadow trees (open, open-stylable etc)).

Option 2

It could be argued that MutationObserver.prototype.observe() is the wrong tool for the job. It was designed to observe mutations under a specific root, and has been since contorted to cater to these types of use cases. Perhaps a different method could be used to signal "observe this everywhere you can", e.g. MutationObserver.prototype.globalObserve().

Anything else?

No response

@LeaVerou LeaVerou added addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest labels May 8, 2024
@annevk annevk added the topic: shadow Relates to shadow trees (as defined in DOM) label May 8, 2024
@WebReflection
Copy link

As author of all polyfills from v0 to current state, I fully support this proposal. My workaround is not super slow but it requires me patching the global attachShadow method on native classes which is a practice I usually avoid at all costs and something I've been advocating against for just about forever ... yet polyfill gotta polyfill, so that if this new proposal is the only thing that might need a greedy polyfill around until everyone is onboard, it's definitively worth the effort to me.

On a side note: the closed shadow might still be addressed somehow and I feel like the elephant in the room here is that we don't have a way for Websites authors to decide what is trusted as script that could patch natives, and what isn't ... CSP is not a great answer to this disambiguation, but maybe this discussion should be done in other venues.

@trusktr
Copy link

trusktr commented May 15, 2024

An alternative to this might be to be able to observe the composed tree, because in some cases we might want to know the nodes that will participate in the final output:

I've had to implement composed tree tracking so that webgl-rendered custom elements would render correct results when composed via slots into shadow roots. I'd like to release some of these tools separate libs, maybe it'll help ideate.

Even without custom rendering, it might be beneficial to react to composed nodes, avoiding extra work for trees that are not active. F.e. suppose a client-side route switcher modifies slot attributes to show/hide content, and we want to observe only content in the active tree.

@trusktr
Copy link

trusktr commented May 15, 2024

Over in

@ox-harris implements options.subtree = 'cross-roots' in the realdom lib for synchronously observing added/removed elements across shadow root boundaries, and gets us a little closer to synchronous observation of the composed tree (f.e. detect composed children, composed parent, etc).

Not only is the usage simpler than with MutationObserver (because MO events fire out of order) and lead to simpler code, but composed tree tracking code will also be as simple when that's ready (ComposedChildrenObserver, ComposedParentObserver, ComposedMutationObserver, etc).

@smaug----
Copy link
Collaborator

@LeaVerou could you explain a bit more what is the use case here?

@LeaVerou
Copy link
Author

LeaVerou commented Jun 3, 2024

@LeaVerou could you explain a bit more what is the use case here?

Sure, what is unclear in my examples?

@smaug----
Copy link
Collaborator

"to implement polyfills of certain features that affect HTML syntax across both light and shadow DOM because it requires observing mutations in shadow trees as well" ... "Any new HTML element or attribute"

So what issue there exactly are you trying to solve?

@LeaVerou
Copy link
Author

LeaVerou commented Jun 6, 2024

"to implement polyfills of certain features that affect HTML syntax across both light and shadow DOM because it requires observing mutations in shadow trees as well" ... "Any new HTML element or attribute"

So what issue there exactly are you trying to solve?

To implement polyfills of new HTML syntax, you need to be able to react to relevant changes in the DOM. E.g. for a new attribute foobar, you need to be able to react when someone adds a foobar attribute anywhere, including deeply nested Shadow DOM. Doing this with the current MutationObserver API is a hack (I listed how in the OP). Does that make sense? If not, it would help to try and formulate an actual question on what part you find confusing.

@smaug----
Copy link
Collaborator

I see, you want to polyfill new global attributes basically.

@LeaVerou
Copy link
Author

I see, you want to polyfill new global attributes basically.

That's one use case. Or new native elements. Or new attributes on native elements.

@LeaVerou
Copy link
Author

Just realized, this wouldn't only help with polyfilling HTML syntax, but also attributes like element.currentLang: whatwg/html#7039

@ox-harris
Copy link

ox-harris commented Sep 23, 2024

Over in

@ox-harris implements options.subtree = 'cross-roots' in the realdom lib for synchronously observing added/removed elements across shadow root boundaries, and gets us a little closer to synchronous observation of the composed tree (f.e. detect composed children, composed parent, etc).

Not only is the usage simpler than with MutationObserver (because MO events fire out of order) and lead to simpler code, but composed tree tracking code will also be as simple when that's ready (ComposedChildrenObserver, ComposedParentObserver, ComposedMutationObserver, etc).

Thanks @trusktr for the mention.

Re: the above, there's been a clear misfit between what I do and what the MutationObserver APi does. For example, I sometimes just want to say:

  • observer the style attribute (or draggable attribute, etc) globally. Which exactly means "also within Shadow trees".
    • do that synchronously, so that I can closely mimic native timing for the behaviour I want to attach to the element.
      • and in some of those cases: do that ahead of native timing; i.e. let me intercept this attribute before the DOM sees it. E.g. to rewrite animation properties in the style attribute before the 'animationstart` event fires.

(A reall case in point: implementing the upcoming OOHTML proposal.)

It turns out to be extremely difficult and mostly impossible to use the MutationObserver API in those scenarios. Writing the above referenced library - RealDOM - a realtime, and more declarative, DOM API, was the logical answer. Now, while I didn't pursue that as a proposal, perhaps some of ideas there could help in the ideation in here.

PSS: I didn't pursue that as a proposal because I was unsure how much typical my usecase was. But I think it's more of a moving story now.

OrKoN added a commit to puppeteer/puppeteer that referenced this issue Oct 3, 2024
The regression is caused by the visible flag no longer
changing the polling type to RAF. This solves the
case reported in the related bug when changes within
shadow roots are tracked with visible=true. The underlying
issue is that MutationPoller does not consider shadow
roots at all. There is a related spec issue to support
shadow roots in MutationObserver but as long as it is
not supported we need a client side solution.

This PR adds a test for that case as well as a test
when visibility is changed without DOM mutations.

Refs: #13152
Related: whatwg/dom#1287
Drive-by: adds a test for mutations inside shadow roots.
OrKoN added a commit to puppeteer/puppeteer that referenced this issue Oct 3, 2024
The regression is caused by the visible flag no longer
changing the polling type to RAF. This solves the
case reported in the related bug when changes within
shadow roots are tracked with visible=true. The underlying
issue is that MutationPoller does not consider shadow
roots at all. There is a related spec issue to support
shadow roots in MutationObserver but as long as it is
not supported we need a client side solution.

This PR adds a test for that case as well as a test
when visibility is changed without DOM mutations.

Refs: #13152
Related: whatwg/dom#1287
Drive-by: adds a test for mutations inside shadow roots.
OrKoN added a commit to puppeteer/puppeteer that referenced this issue Oct 7, 2024
The regression is caused by the visible flag no longer
changing the polling type to RAF. This solves the
case reported in the related bug when changes within
shadow roots are tracked with visible=true. The underlying
issue is that MutationPoller does not consider shadow
roots at all. There is a related spec issue to support
shadow roots in MutationObserver but as long as it is
not supported we need a client side solution.

This PR adds a test for that case as well as a test
when visibility is changed without DOM mutations.

Refs: #13152
Related: whatwg/dom#1287
Drive-by: adds a test for mutations inside shadow roots.
OrKoN added a commit to puppeteer/puppeteer that referenced this issue Oct 7, 2024
The regression is caused by the visible flag no longer
changing the polling type to RAF. This solves the
case reported in the related bug when changes within
shadow roots are tracked with visible=true. The underlying
issue is that MutationPoller does not consider shadow
roots at all. There is a related spec issue to support
shadow roots in MutationObserver but as long as it is
not supported we need a client side solution.

This PR adds a test for that case as well as a test
when visibility is changed without DOM mutations.

Refs: #13152
Related: whatwg/dom#1287
Drive-by: adds a test for mutations inside shadow roots.
@patrick-mcdougle
Copy link

Just in case another use case could inspire why this could be compelling...

Let's say I have a component auto-loader (like shoelace autoload) that detects custom elements being inserted into the DOM using MutationObserver. It'd be swell if some of my custom components (or perhaps 3rd party components) that use shoelace components in open shadowTrees could also trigger the auto-loading mechanism.

@WebReflection
Copy link

As WebKit / Safari is landing scoped Custom Elements we should be careful with polyfills or observing custom elements in there because the inner registry might already know, or take care of, those elements ... so that <shoe-thing> in there might not need to be bootstrapped, when in ShadowRoot, like the <shoe-thing> on the main light-dom, actually this is a very risky thing to do for whoever is dealing with patched attachShadow too, if that logic assumes all those potential custom elements require outer main DOM intervention.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest topic: shadow Relates to shadow trees (as defined in DOM)
Development

No branches or pull requests

7 participants