Replies: 1 comment 1 reply
-
Thanks for raising this @dgp1130 though I think for now this may better fit into the Discussion format, to help thread out the various topics and proposals, but also since I think it will take a little time for the project to get to this point timeline wise. My only familiarity / naiveté on this topic from an implementation perspective comes is my awareness that from a userland perspective developer would need to de-mark the serialization points using something special like I do have some work on started on using an optional JSX based Would definitely be open to seeing if there's any viability in making this a WCCG community protocol as well. Some initial conversations in this sort of advanced rendering / hydrating / loading have popped up in this issue - webcomponents-cg/community-protocols#30. (apologies if I may have alreay shared this one with you before) |
Beta Was this translation helpful? Give feedback.
-
Type of Change
Feature
Summary
Could Greenwood support resumability?
Details
I was recently looking into resumability in the context of HydroActive, but ultimately decided it wasn't really a good fit, as it requires a certain level of client/server integration HydroActive is too decoupled to achieve. However, as I started to think about how it could work for custom elements with
defer-hydration
support, I suspect Greenwood might be a better fit for this idea. It might be a little early for this discussion as I don't think you've looked into hydration much so far, but I think it's a cool idea and I wanted to write it down. Apologies for the length of this issue. 😅TBH, I'm still not sure I'm totally following all the concepts of resumability and what it's key requirements are, but the main feature I think it provides is deferring download of event handlers until those events trigger. Is it possible to achieve this with custom elements in Greenwood?
I think the answer might be yes. We need a few things to do this:
my-component
can be defined by importing./src/components/my-component.js
."click
event triggers onbutton#my-button
, please loadmy-component#1234
which will respond to that event."click
event occurs on the page..."click
events frombutton#my-button
."Let's break down each one.
Component definition map
The first problem is to map component tag names to the URLs which can define them. I think this can be solved with WCC or a separate build-time tool. WCC can identify all the custom elements in a compilation and link to the path which will load them at runtime. In a bundled application, each component would need to be an entry point and this would need to link to the chunk which defines the component (
./chunk-abc123.js
). This should be able to generate a map of custom elements to a JavaScript file you can import which will define that element.The challenge here is that I think this needs to be done at build time and integrate with any bundler (pass each component as entry points and track the output mapping). That means that WCC needs to work statically on the application source code, which I don't think is how it works today. I suspect this might be as simple as grepping for
customElements.define('my-component', /* ... */);
and then mappingmy-component
to the file you find that define in. However there might be edge cases to deal with here. Maybe it makes sense for a different build-time tool to do this?Component event map
The second problem is to map the pair of event target (
HTMLButtonElement
) and event name (click
) to the custom element (MyComponent
) which will handle that event. This can be addressed through a monkey-patch of the server runtime. We can define a patch ofEventTarget.prototype.addEventListener
which is functionally a no-op (because most events can't really trigger in an SSR context) but which records all calls to it. This record is serialized into the output HTML by tracking the custom element which is currently rendering, and the event it wanted to listen to.From the user's perspective, they might write this component:
What this says is that "the currently rendering
MyComponent
element cares about theclick
event of thisHTMLButtonElement
". Greenwood would track this at runtime and render out the content:This can be implemented through a monkey patch of
EventTarget.prototype.addEventListener
which looks like this:This solves the second problem by serializing the information of which components are interested in handling events from which elements.
data-greenwood-specifier
comes from the mapping we generated in step 1 by looking up themy-component
tag name of the currently rendering element.Global event handler
The third problem is much more straightforward, we need to listen for any event which might require a component to be downloaded.
This script needs to be synchronous so we don't miss events triggered by the user before the global event handler is loaded. Don't worry, it will be a small script and shouldn't have a significant performance impact.
Dynamically load components
Fourth, we need to implement this global handler such that it identifies the component which should have received the event, dynamically loads that component, and then replays the event.
This would trigger the
addEventListener
callback and increment the count onMyComponent
!I think this would roughly work. It would download the correct component, define it, hydrate the element, and then replay the event and respond to it. Yet all of that work is done lazily. Only the global event handler needs to be loaded eagerly, and that doesn't depend on any component JavaScript, so it should be a fairly constant size, no matter how large the application actually is.
With
defer-hydration
you get the additional benefit that ifmy-component
catches an event, is downloaded, and then hydrated, othermy-component
elements can still be deferred and only lazily hydrated by the global event handler.The biggest challenge that I've skirted over here is mapping the event handler to the host which registered it.
EventTarget.prototype.addEventListener
doesn't actually have enough information to do that. It doesn't know which component created the event handler. The best approach I can think of is to track the currently rendering component stack (something Greenwood might already do?). If we have a stack of components which are actively rendering, then we know the last component in the stack has control right now, so anaddEventListener
call should be associated with that component.I'm not sure that's a great model and it would probably fall apparent for async operations. IIRC, Lit batches DOM updates and applies them asynchronously. If that includes adding event listeners, then I'm not sure how you'd associate the
addEventListener
call with the component which Greenwoord rendered in a different async stack frame. You'd need something like async context or Zone.js to do that. MaybeAsyncLocalStorage
would be a way to do this today in Node, but I suspect other cloud provider runtimes don't support this particular feature.I'm taking a few other liberties here in the implementation which would need to be ironed out:
[data-greenwood-id]="1234"
if it's under a shadow root?my-component
is going to import its dependencies which will all be in different chunks by nature of every component being an entry point to the bundler. As a result, it's probably important to track dependencies such that you can preload those dependencies and parallelize their download.modulepreload
tags to load components eagerly, even if they get executed lazily.data-greenwood-specifier
could likely be deduplicated. It maps tag names to specifiers, so you don't need every element instance to point to the specifier. This mapping could be stored in a separate JSON format inlined in the HTML.I'm not sure if this is technically "resumability". Probably Misko is the only one who could definitively say that. It does defer event handler loading until after the events which trigger them, which I think is the main functional requirement. Where it might fall short is that you can't load and execute individual event handlers within the same component. You can only load a single component and all of its event handlers at minimum. In a web components world, I think that's perfectly fine, but I don't know if event handler specificity is a strict requirement for resumability. Also the intent of "resuming execution by not repeating any work the server already did" isn't exactly followed since the component will need to re-render itself (unless it support some kind of hydration/resumability mechanism itself). As such, this is maybe more "resumable loading" rather than "resuming execution".
Anyways, I think this could be a cool feature which could potentially work with any web component implementation and also benefit from the client/server integration which Greenwood provides. Hope this is an interesting idea to explore once you start diving into hydration!
Beta Was this translation helpful? Give feedback.
All reactions