Allow customElementRegistry definitions to persist across page loads
andy-blum opened this issue · comments
One of the biggest drawbacks of using web components is the flash of unstyled content (FOUC) following the page load but prior to web components being defined. Additionally, when loading multiple web components as modules pages go through a considerable amount of cumulative layout shift as each component is defined, styles are reevaluated and layout is recalculated.
While server-side rendering is an option, it has several drawbacks:
- It requires server configuration and resources. This can be a considerable barrier to use on what is fundamentally a client-side platform feature, especially when a single component library design system is used across multiple projects under a single domain
- Some approaches move component internals outside of shadowRoot, breaking style encapsulation
- Component stylesheets have to be passed through style nodes instead of constructable/adoptable stylesheets
- It requires a hydration step on the front-end, meaning there's still a delay to interactivity for components
It seems to me all these problems could be mitigated by allowing component definitions to persist across page loads within the same domain, similar to how data can be stored client-side in sessionStorage and localStorage. In this way, a first visit to a site using web components bears the weight of LCP & CLS metrics, but each successive page load is faster as the components are reused.
I wonder if this is something that could be added into the scoped customElementRegistry proposal, @justinfagnani?
In which JS realm would the component definitions live?
I think we need declarative custom elements for this. Implementations could cache at least the source code for definitions (possibly even in pre-parsed/binary format, very similarly what Firefox used to do with XBL).
I think we should start with a more general-purpose mechanism to cache the pre-parsed/binary format of a given library. That seems like a pre-requisite to doing something Web Components specific.
I was initially thinking this would allow storing element instances and re-using them across page loads (having their adoptedCallback
s be called once placed into the document across the other side of a page load).
This would require some semantics of Service worker, with considerations for how to bust the cache on app updates. Maybe a ServiceWorker would even be responsible for this, like maybe it would manage a pool of elements. Maybe it would only be position if an element does not reference anything from its previous Realm, and only grabs things from the new document
at adoptedCallback
time, otherwise the element would simply be GC'd and would not reach the SW's pool. Upon new page load, when a new element is instantiated on the main thread, the engine can return an element of matching class provided by the SW. A SW could also persist some elements while the app isn't even open, ready for later.
When an element would be garbage collected (or when the page is being destroyed) this would be an opportunity for some API in the SW to be called with the element that is no longer needed in the main thread.
Totally just imagining here, not really sure how feasible this is, but the idea seems interesting.
Piggybacking on idea...
Looks like we are on the edge of breaking the DOM passing across threads prohibition rule. The WC Library ( and in general any ) could have a use for bundle globals initialization and keep as registry for this lib as the implementations. Lib URL would serve the artificial context(hidden page/thread). The consumer page would refer the lib by URL on one of layers enabling its custom elements there.
The life cycle would start with 1st use and close upon session termination with ability to discard any time on browser discression.
Of cource there would be no domain nor document in the scope of WCL. Those would become available only during WC instantiation. Such restrictions can be lifted if lib domain and consumer page match.
WCL would encourange CDN kind deployments and reuse. With security ^^ in place,
preloading
as usual via meta
tag
embedding into scope of WC
- declarative - same
meta
resource? - imerative - import as JS module. Perhaps with own
assert
-ion.
dedicated content-type=wcl
would allow to omit the special syntax and tags. Instead, resource with this content type would be treated accordingly.
WCL content
The actual payload of customElementRegistry plus (internal?) tag to url+implementation. The dual interface for declarative(HTML/XML) and imperative lingo. Just to be short, just a JS version:
{
'my-element': { 'module':'rel-path/my-el.js', implementation : class MyElement{...} }
}
import maps/ relative path
Of cource module
is a subject for import mapswhen loaded by page. Lib could have own import maps for internal sub-modules as independent deployment unit on CDN.
WCL can load another WCL.
tag maps
Those are needed to make a map of mapping between internal tags and used by caller context.
It requires server configuration and resources.
Not necessary. If we deine imperative registry API as self-registering in current context, there would be no need for any server side special handling. Plain import JS module with payload like following suffice:
// WCL
export defalut function register( contextComponent, tagmap={} ){
contextComponent.define(tagMap["my-custom-element"] || "my-custom-element", class MyCustomElement{ ... } )
contextComponent.define(tagMap["my-another-element"] || "my-another-element", class MyLuckyElement{ ... } )
}
// caller
import someLib from 'path/wcl.js'
// within component ...
someLib.register(this)
disclaimer: syntax is not exact, just an idea
I'm not really understanding how FOUC is related to web components. FOUC is a general problem which can occur if the parser yields and paints before any expected state, either before some script that performs rendering has run or even before the HTML/CSS has all streamed in.
And, typically upon page-refresh the browser's cache will pull in these resources which should typically result in no FOUC.