polyfills icon indicating copy to clipboard operation
polyfills copied to clipboard

[scoped-custom-element-registry] proof of concept to fix stand-in element issues

Open sorvell opened this issue 1 year ago • 6 comments

Description

prototype new polyfill

This is an alternate way to implement the custom elements registry polyfill. Instead of polyfilling CustomElementsRegistry completely and using a stand-in proxy element that delegates to the proper scope, real scopes are created via the CustomElementsRegistry available in an <iframe>, with one <iframe> per registry. This avoids the need to have a stand-in element proxy and the associated issues.

Motivation

Address issues that result from the stand-in element approach, e.g.

  • https://github.com/webcomponents/polyfills/issues/560
  • https://github.com/webcomponents/polyfills/issues/546
  • https://github.com/webcomponents/polyfills/issues/569

Caveats

  • This approach requires being able to create an iframe and get access to its contentWindow and associated globals; this may have issues with some CSP settings (needs research).
  • FIrefox seems to have some issues with torn off element functions (e.g. connectedCallback) being called in the correct scope. This is worked around currently, but a better fix should be researched.
  • Correctly polyfilling such that elements can move between the iframe scope and main document is tricky and the prototype may expose some corner cases.

sorvell avatar Nov 27 '23 20:11 sorvell

@sorvell small sugestion what about making window.CustomElementRegistry bit more defensive so it doenst overwrite when the polyfill gets loaded more then once. i think this is also a problem with the legacy version if i read the code correctly

pascalvos avatar Nov 27 '23 21:11 pascalvos

I think this approach is looking viable. The POC version now passes the tests at https://github.com/open-wc/open-wc/blob/master/packages/scoped-elements.

The biggest known issues are:

  • Creating iframes is slow. On an M1 Mac, Chrome is about 1.5ms per, while Safari/FF are about 1/3 that. Fix: maintain a virtual registry and use actual registries only when there are conflicts so it's pay for play. The trade off is that customization is more expensive since an element must be in the registry's document to customize/upgrade. This means potentially moving elements to other documents so that they customize. Mitigation: if the number of conflicts is low, the amount of movement is small and this seems like a reasonable hypothesis; in addition, moving DOM is required to address the next issue.
  • Lazy registrations require moving elements to the registry document to customize/upgrade. This creates spurious custom element callbacks of connected/disconnected/adopted as the element is moved. Fix: patch element constructors and squelch the spurious callbacks.
  • Firefox double loads iframes async. Fix: using frame.contentWindow.stop() seems to work ok.

sorvell avatar Dec 01 '23 01:12 sorvell

Fix: patch element constructors and squelch the spurious callbacks.

Would this affect all custom elements on a page or only those that opt in to scoped registries? Patching all custom elements is potentially problematic as that can have unintended consequences outside of Lit.

Only scoped elements to be patched.

And, since this is intended as a polyfill, the goal is to patch in a way that does not rely on any library (e.g. Lit).

sorvell avatar Dec 01 '23 14:12 sorvell

if you would rely it on lit would that make things more easy or the end result be the same ? would the lit version have better optimization benefits if there was such a thing? i ask this cause of the existence of @lit-labs/scoped-registry-mixin package

vospascal avatar Dec 03 '23 21:12 vospascal

Since the spec is still likely in flux and the first implementation is in progress, work has started on making this into a ponyfill. There are some pros and cons to this, captured below:

Ponyfill v. Polyfill

Consideration Polyfill Ponyfill
Affects all elements Yes No
Performance concerns More Less
Requires bespoke API No Yes
Bespoke API for attachShadow No Yes
Bespoke API for innerHTML, etc. No Yes / Optional
Native feature interop Yes No
Removes seamlessly Yes No
Can differ from native feature No Yes
Better with 3rd party code Yes No

Approaches

Overview

  • stand-in (current): A proxy stand-in element is defined and the proper scoped constructor is swapped in at creation time; therefore what you define is not what you get.
  • iframe (new): A native registry is used in an iframe and elements are customized in the iframe and moved to the main document; therefore what you define is what you get.

Form Associated

  • stand-in (current): all stand-ins are form associated so they can -incorrectly be focused (on Safari) or be disabled for clicks
  • iframe (new): works as expected

Attributes

  • stand-in (current): .attributes - does not work, bespoke implementation requires maintenance
  • iframe (new): works as expected

Late Define (uncommon!)

  • stand-in (current): requires manual upgrade only if defined elsewhere, candidates known
  • iframe (new): requires manual upgrade, candidates must be located in all scopes

Interaction with global registry

  • stand-in (current): global registry applies in scopes; -impacts global registry, definition cooperation
  • iframe (new): global registry applies in scopes; no interaction with global registry

Performance: registry creation

  • stand-in (current): cheap, just object creation
  • iframe (new): -expensive since it's backed by an iframe (~1ms on M1); heuristic minimizes iframes so in practice there are typically < 10

Performance: creation

  • stand-in (current): fast, uses native customization
  • iframe (new): fast, uses native customization

Performance: upgrades (uncommon!)

  • stand-in (current): fast, uses native customization
  • iframe (new): -slow, must move elements to iframe scope and back (only necessary for late define, not innerHTML, createElement)

Alternatives

Elements can applly manual scoping using only the global registry by ensuring all elements are registered with unique names. This would work as follows:

  1. import custom element classes
  2. create trivial subclasses for each (since a given class can only be defined once)
  3. define each element at a unique name, this would likely need to be version specific, e.g. md-button-1.0.1. This might be fine but to be 100% safe the name would have to be generated in-situ using customElements.get.
  4. adjust all usage to use the unique name.
    • In a library like Lit creating elements with static-html could make this relatively seamless, or even more bespoke support like a 1x template tag replacement could be provided.
    • other than in HTML, referencing elements by tag name in css or DOM API (e.g. querySelector) is best avoided.

sorvell avatar Dec 14 '23 20:12 sorvell

Approach was abandoned due to complexity related mostly to the need to do manual upgrades for late definitions.

sorvell avatar May 08 '24 15:05 sorvell