-
Notifications
You must be signed in to change notification settings - Fork 11
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
[progressive-hydration] conditions for first level hydration timing #30
Comments
might make it feel more "in line" with the web? <img loading="lazy" />
<my-list loading="client"></my-list>
<my-list loading="hydrate"></my-list>
<my-heavy-chart loading="hydrate:onVisible || onMedia('(min-width: 768px)')"></my-heavy-chart> |
+1 with loading! |
This probably should be combined with defer-hydration. Basically, everything is For "parents" that will never hydrate on their own but only "as needed" based on their children it could be something like Additionally, there is now a need to define that the hydration should wait on "what"? A suggestion would be to rename all "modifiers" to
which results in <my-form loading="hydrate:asNeeded" defer-hydration>
<my-input loading="hydrate:onClick && withParentFirst" defer-hydration></my-input>
</my-form> Now if you click inside
|
does/should this proposal also come with a way to tell when a component has been hydrated? stencil has the Could there be an eventing strategy so that a hydrated component could notify other components when it gets hydrated in like a pub/sub kind of way? |
Good question 🤔 In my current implementation, I test it by using a component that "manually" exposes it. in import { LitElement, html, css } from 'lit';
export class MyEl extends LitElement {
static properties = {
msg: { type: String },
hydrated: { type: Boolean, reflect: true },
};
constructor() {
super();
this.hydrated = false;
}
updated(props) {
super.updated(props);
this.hydrated = true;
}
render() {
return html`<p>Hello World</p>`;
}
static styles = css`
:host([hydrated]) {
background: green;
display: block;
}
`;
} but right - the "HydrationLoader" (at least that's how it's called in my current implementation) could add the attribute after the component is loaded, registered and upgraded... and it could fire an I'm a little wary of the scope - so I would call these "optional" goals for now - as they look more like nice to have (especially as we could add them later) - don't get me wrong if there is fast agreement on all the other stuff we could include it - or it could be an iteration 🤗 |
Looks neat! Thing is that we may not want to confuse what DOM string templates from the "what looks like HTML" that becomes ECMAScript code (e.g. JSX, Vue template, Lit's So if we want two types of condition, separate the "loading" per input type. And keep strings as strings. Using the string as a "channel" is looking good though. Can't we find a way to make the "loading" be one for a name, the other for MQ. But there's How about we do something like <e
loading="whatever"
media="(max-width: 320px)"
/> So we tell the host that this component has a "loading" so we broadcast it subscribe to it. Then have the window subscribe for the conditional in window.addEventListener('loading:whatever', (e) => { /* ... */ })
// Yeah, this ^ bugs me about addEventListener.
// Don't want to mixup namespacing and passing an argument to.
// Maybe have another way. Like instantiating a "service" and hook it up as a consumer when "loading" occurs. |
Would it make sense to rename the title of this issue to progressive hydration? Following along with chats on Twitter about this and assuming we would all abide by Ryan / Misko / etc acknowledgement of these definitions, I then think partial hydration would be an entirely different proposal / protocol than progressive hydration. (IMO) |
I really try to follow all these hydration discussions - but still, none of the explanations made it clear to me what defines a hard cut between full, partial, progressive hydration or resumeability. imho it's quite a blurry line 😅 but yeah this seems more like "progressive hydration"... as it's not only about hydration but also about loading code (why that is not important for all of the hydration strategies I don't really understand 🙈) |
Yeah, it has taken me a little while to try and wrap my arms around them all as well, especially all their pros / cons and subtle implementation details. though I definitely do not claim to be an expert at the time of writing this, here's my best take on what it all means. 😅
So using the above definitions, and given this bit of code that has some state and some event handling, I'll try and apply the expected output for each of those techniques as best I can to reflect their distinctions. class Counter extends HTMLElement {
constructor() {
super();
this.count = 0;
if(this.shadowRoot) {
this.shadowRoot.querySelector('button#dec').addEventListener('click', this.dec.bind(this));
this.shadowRoot.querySelector('button#inc').addEventListener('click', this.inc.bind(this));
} else {
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = this.render();
}
}
inc() {
this.count = this.count + 1;
this.update();
}
dec() {
this.count = this.count - 1;
this.update();
}
update() {
this.shadowRoot.querySelector('span#count').textContent = this.count;
}
render() {
return `
<div>
<button id="inc">Increment</button>
<span>Current Count: <span id="count">${this.count}</span></span>
<button id="dec">Decrement</button>
</div>
`;
}
}
customElements.define('wcc-counter', Counter); Progressive Hydration
Basically what we're talking about here, where the loading of the JS is not eagerly done on load, but rather via something like an IntersectionObserver or MutationObserver. (Think Astro and its islands) The two big issue with top down hydration are:
There is a lot of nuance in this section when it comes to top down (like React or Lit) and their Partial Hydration
Now this is where things start to get interesting, and where something like React Server Components come into the picture. As mentioned with hydration, we're often finding ourselves shipping the work and the data from the server to the client again, to keep things in sync. So what if the server code that ran and wasn't needed again on the client (like say just rendering the template) didn't get shipped? So in a partial hydration scenario, a compiler or build tool could examine our class Counter extends HTMLElement {
constructor() {
super();
this.count = 0;
if(this.shadowRoot) {
this.shadowRoot.querySelector('button#dec').addEventListener('click', this.dec.bind(this));
this.shadowRoot.querySelector('button#inc').addEventListener('click', this.inc.bind(this));
} else {
this.attachShadow({ mode: 'open' });
}
}
inc() {
this.count = this.count + 1;
this.update();
}
dec() {
this.count = this.count - 1;
this.update();
}
update() {
this.shadowRoot.querySelector('span#count').textContent = this.count;
}
}
customElements.define('wcc-counter', Counter); This effectively aims to solve the double data / JS issue with top down hydration, but may or may not incur a little initial runtime overhead to glue some of these pieces together. (I think Solid does this) Resumable
In this case, we're now going in a completely different direction from hydration, so much so that per Misko, we shouldn't be thinking of resumable as hydration at all. So, given our starting <div>
<button id="inc" onclick="() => this.count + 1">Increment</button>
<span>Current Count: <span id="count">${this.count}</span></span>
<button id="dec" onclick="() => this.count - 1">Decrement</button>
</div> This is why Qwik is Progressive and Resumable, but not Partial. The And so this is what is meant by resumable; in that you could copy / paste the active HTML of this output at any time and paste it into the document of another app as Anyway, that's where I am at so far with all of these, and I'm sure I missed a bunch of nuance and not nearly all the pros / cons, but I can't do it any justice the way Ryan Carniato of Solid / Marko does it, so I'll just link to his blog posts and live streams, which is where all my knowledge has come from effectively. I think some of these have interesting promise as community protocols, since given how much the latter two solutions require a pretty complex system / framework to operate in, having bespoke implementations all over the place could be tough for developers moving between projects. So if we can align on terms, and maybe even some interfaces, I think at least portability of concepts, and hopefully code, can be achievable without getting in the way. ✌️ |
Some of my own perspective:
|
@thescientist13thank you for writing this summary - especially the code sample that made it way more understandable to me. As far as I understand most of these things will only work for a "full framework" that is in full control of the whole rendering server & client side (e.g. it needs to know all possible way that can trigger a change or an interaction). Seems not such a good fit for web components? especially with shadow dom and strong encapsulation in mind? @matthewpthank you for taking a look 🤗
Available in alpha version of RocketThe above implementation has been released in Try it for yourself 💪 👨💻 and select the "Hydration Starter" Twitter Announcement: https://twitter.com/daKmoR/status/1519263600371814400?s=20&t=ZqeIxf-_s0lQ0tT-Y6OD0g Docs: https://twitter.com/daKmoR/status/1519263600371814400?s=20&t=ZqeIxf-_s0lQ0tT-Y6OD0g |
Yes, to my knowledge all these strategies, hydration included, imply some sort of orchestration on the client side because they are all in some way building on top of what has already been done on the server for rendering. The issue then with current implementation of hydration include doubling up of the work on the client side, including:
(Perhaps how hydration markers are implemented could be a community protocol ??) I think what's worth taking away from these other approaches is seeing if we can do better than top-down hydration because it is effectively a lot of duplicate work, even if in the client side it is "just attaching event handlers". Now, not saying it is not a viable strategy, or one worth supporting because I'm not sure any of these strategies (maybe Resumable since it is not hydration per se) are a silver bullet, but I think it's interesting to explore just how much we can take advantage of what the server has already done to avoid doubling up any of that work on client. I think it could end being a combination of a couple of these strategies in the end though since you will definitely need at least a single pass on the server, streaming or not. I think if anything, Resumable is great in theory, but it further pushes your code into framework land, and is very manual by the author. At least a community protocol here could standardize on that implementation detail so WC authors don't have to think about the mechanims and their relative portability. I think it is definitely a fair point to call out that typically the more fine-grained you want to go, the more DSL-y your code becomes, and is very intertwined with the framework, etc. So cool output, but I definitely worry about the vendor lock-in nature to any of these attribute based implementations, which is what inspired me to explore #33 . In my mind at least, I think this is something WCs could work really well for, because Shadow DOM, and in particular Declarative Shadow DOM provides a great encapsulation mechanism already built into the browser, and can easily be baked into HTML, like in a Admittedly I'm still exploring the space and I know Ryan Carniato ran into certain issues using WCs to implement his earlier version of SolidJS, but I haven't looked at his work in that space deeply enough to know if what he encountered was an issue at the spec level, or how he wanted to implement fine-grained reactivity, or something else entirely Maybe I'll reach out to him. So yeah, I guess technically at this moment I don't know what specifically would prevent WCs from participating in all these various strategies and captured as community protocol, but I'm here to see what is possible. 🤓 |
First Level hydration
This is a proposal for a "syntax" of conditions to trigger hydration of a component.
It has 3 separate "states":
For
server/client
there are no additional "options"... but for hydrate, there are multiple modifies you could combine[1]: global events: implemented via a single global event handler
[2]: element events: every element needs its own event handler
[3]: modifiers: modify how/when the hydration happens AFTER all conditions are met
Hydrate condition combinations
Each of the options can be combined via
&&
or||
.loading="server"
loading="hydrate"
loading="hydrate:onIdle"
loading="hydrate:onClientLoad"
loading="hydrate:onMedia('(max-width: 320px)')"
loading="hydrate:onMedia('(min-width: 640px)') && onClick"
loading="hydrate:onMedia('(prefers-reduced-motion: no-preference)') && onClick"
loading="hydrate:onVisible && onIdle"
loading="hydrate:onVisible(100px)"
loading="client"
sadly this does not prevent "useless" combinations like
loading="hydrate:onVisible && onClick && onHover"
.Inspired by withastro/roadmap#108 and slinkity/slinkity#20
Example of user code
The text was updated successfully, but these errors were encountered: