polyfills
polyfills copied to clipboard
[scoped-custom-element-registry] proof of concept to fix stand-in element issues
Description
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
contentWindowand 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 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
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.
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).
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
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
-incorrectlybe 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;
-impactsglobal 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):
-expensivesince 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:
- import custom element classes
- create trivial subclasses for each (since a given class can only be defined once)
- 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 usingcustomElements.get. - adjust all usage to use the unique name.
- In a library like Lit creating elements with
static-htmlcould 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.
- In a library like Lit creating elements with
Approach was abandoned due to complexity related mostly to the need to do manual upgrades for late definitions.