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

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

Closed
sorvell opened this issue Nov 27, 2023 · 7 comments

Comments

@sorvell
Copy link
Collaborator

sorvell commented Nov 27, 2023

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.

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 sorvell changed the title [custom-registries] Proof of Concept to fix stand-in element issues [scoped-custom-element-registry] Proof of Concept to fix stand-in element issues Nov 27, 2023
@sorvell sorvell changed the title [scoped-custom-element-registry] Proof of Concept to fix stand-in element issues [scoped-custom-element-registry] proof of concept to fix stand-in element issues Nov 27, 2023
@pascalvos
Copy link

@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

@sorvell
Copy link
Collaborator Author

sorvell commented Dec 1, 2023

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.

@filimon-danopoulos-stratsys

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.

@sorvell
Copy link
Collaborator Author

sorvell commented Dec 1, 2023

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).

@vospascal
Copy link

vospascal commented Dec 3, 2023

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

@sorvell
Copy link
Collaborator Author

sorvell commented Dec 14, 2023

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
Copy link
Collaborator Author

sorvell commented May 8, 2024

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

@sorvell sorvell closed this as completed May 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants