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

[defer-hydration] Initial draft #15

Merged
merged 6 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ Check out the [Issues](https://github.com/webcomponents/community-protocols/issu

## Proposals

| Proposal | Author | Status |
|-----------------|----------------|--------|
| [Pending Task] | Justin Fagnani | Draft |
| Proposal | Author | Status |
|-------------------|----------------|----------|
| [Pending Task] | Justin Fagnani | Draft |
| [Defer Hydration] | Justin Fagnani | Proposal |

[Pending Task]: https://github.com/webcomponents/community-protocols/pull/1
[Defer Hydration]: https://github.com/webcomponents/community-protocols/pull/15

## Status

Expand Down
90 changes: 90 additions & 0 deletions proposals/defer-hydration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Defer Hydration Protocol

An open protocol for controlling hydration on the client.

Author: Justin Fagnani

Status: Draft
justinfagnani marked this conversation as resolved.
Show resolved Hide resolved

Last update: 2021-06-24

# Background

In server-side rendered (SSR) applications, the process of a component running code to re-associate its template with the server-rendered DOM is called "hydration". The defer-hydration protocol is design to allow controler over hydration to solve two related problems:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple minor grammatical point(s)

change from

The defer-hydration protocol is design to allow controler over hydration to solve two related problems:

to

The defer-hydration protocol is design designed to allow controler control over hydration to solve two related problems:


1. Interoperable incremental hydration across web components. Componentents should not automatically hydrate upon being defined, but wait for a signal from their parent or other coordinator.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why this is (why they shouldn't automatically hydrate)? Is there an example where doing so would create unwanted results?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could see this fitting into a "when work gets done" topic of conversation by allowing islands to be linked generally by component name, while still being able to disambiguate on which of those islands are needed at any one time. Much like Astrobuild leverages the :visible modifier to control when the code for a component is loaded, this could allow other instances of the component on the same page the ability upgrade with the initially visible instance, and then wait to hydrate until they, too, are in view.

I think this sort of laziness deserves to be revisited more thoroughly a the browser spec level. This seems roughly in line with the a proposal around lazy definitions: WICG/webcomponents#782 and could arguably fit into a larger conversation around the browser allowing for lazy upgrades, which would be somewhat similar to what this protocol looks to support.

Regardless, if you're SSR'ing the entirety of an element that may not come into view, the ability to control when it reconciles that delivered SSR render with the client-side render sounds like a good idea.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for just now getting back to this, but why would you want to delay rendering a component? The reason for those modifiers are to:

  1. Deprioritize loading of the code so that more important assets can be loaded over network.
  2. Deprioritize executing the component (and its dependencies) JS so that more important assets can use the CPU instead.

The reason is not to delay rendering. Rendering should be relatively cheap, especially if the component was prerendered, there's no work to do but set up the event listeners.

It is, of course, possible that some components are going to be more expensive than others, but that's reason for those particular components to have their own API to delay rendering, not to make it a standard that is used arbitrarily.

The goal is to make the page as interactive as possible. Delaying rendering without good reason should be avoided.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, the main reason is to enable top-down initialization, even for spontaneous first renders. We need our components to initialize in the same order whether CSR'ed or SSR'ed, independent of definition order.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a different use case (being discussed below). The island orchestration use case is another, it's why Eleventy/webc has adopted this for example.

2. Hydration ordering independent of definition order. Because components usually depend on data from their parent, and the parent won't usually set data on a child until it's hydrated, we need hydration to occur in a top-down order.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is maybe a partial answer for (1)? Should these be separate points, if so?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The more I think about it, I don't think I agree with this point (2), cc @Westbrook. I would expect that in most cases an element's initial data has been serialized as attributes and doesn't need the parent to pass that information. The exception would be data that isn't serialized, like complex objects, but I don't think that's really the norm.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The https://github.com/w3c/webcomponents-cg has a breakout session to discuss Community Protocols, that could certainly include this, at the beginning of next month. Would you be interested in joining the call? Would be great to get your thoughts on this, and many other of the topics we're discussing right now "in person"!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I could probably attend that.

Just for clarity, I'm not against this proposal, just trying to understand the scenarios in which it is needed. The text could help by clarifying that a bit more.

My thinking is that a typical usage would not need this because:

  1. Static imports take care of making sure children are hydrated first.
  2. Even if hydration happens out-of-order state should be serialized in attributes.


`defer-hydration` enables us to decouple loading the code for web component definitions from starting the work of hydration, and enables top-down ordering and sophisticated coordination of hydration, including triggering hydration only on interaction or data changes for specific components.

# Overview

The Defer Hydration Protocol specifies an attribute named `defer-hydration` that is placed on elements, usually during server rendering, to tell them not to hydrate when they are upgraded. Removing this attribute is a signal that the element should hydrate. Elements can observe the attribute being removed via `observedAttribute` and `attributeChangedCallback`.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious on folks perspective around defer-hydration as compared to something akin to loading="lazy"

If the attribute was, say, hydration, with options like:

  • defer: Hydrates based on internal behavior
  • lazy: Hydrates based on when in viewport
  • eager: Default behavior

Should the attribute control both the intended behavior and the current state? Some tools use :visible, Stencil uses the hydrated class for hydrated state.

Decoupling this and setting a convention around the intended behavior and leaving how state is communicated up to the tools might be helpful.


When an element hydrates it can remove the `defer-hydration` attribute from its shadow children to hydrate them, or keep the attribute if itself can determine a more optimal time to hydrate all or certain children. By making the parent responsible for removing the `defer-hydration` attribute from it's children, we ensure top-down ordering.

## Use case 1: Auto-hydration with top-down odering

In this use case we want to page to hydrate as soon as elements are defined, but we want to force top-down ordering to avoid invalid child states. Here we configure the server-rendering step to add the `defer-hydration` attribute to all elements _except_ the top-most defer-hydration-aware elements in the document.

When the top-most elements are defined, they will run their hydrations steps since they don't have a `defer-hydration` attribute, and will trigger their subtrees to hydrate by removing `defer-hydration` from children.

Example HTML:

```html
<!doctype html>
<html>
<head>
<script type="module" src="./app.js"></script>
</head>
<body>
<x-nav>
<template shadowroot="open">
<x-header defer-hydration>
<template shadowroot="open">
<h1>Example</h1>
</template>
</header>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
</header>
</x-header>

</template>
justinfagnani marked this conversation as resolved.
Show resolved Hide resolved
<x-article>
<template shadowroot="open">
<x-figure defer-hydration>...</x-figure>
</template>
</x-article>
</body>
<html>
```

## Use case 2: On-demand hydration

Hydration can be deferred until some data or user-driven signal, such as interacting with an element. In this case server-rendering is configured to add `defer-hydration` to all elements so that nothing will automatically hydrate.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

visibility is such a hot/important topic for upgrade/hydration that we should probably include it in line with such as interacting with an element.


An app-level coordinator may implement an event delegation/buffering/replay system to detect user-events within an element and remove `defer-hydration` on demand before replaying events.

*TODO: do we need to have an event that signals that hydration is complete before replaying events?*

## Hydrating children

```ts
class MyElement extends HTMLElement {
static observedAttributes = ['defer-hydration'];

attributeChangedCallback(name, oldValue, newValue) {
if (name === 'defer-hydration' && newValue === null) {
this._hydrate();
}
}

_hydrate() {
// do template hydrate work
// ...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that's not clear to me from this proposal: what does "hydration" mean? Just connectedCallback? Or constructor plus connectedCallback?

I'm assuming that it only means connectedCallback, but you could imagine that a web component could have some logic in its constructor that assumes client-side rendering, which assumes top-down rendering, and thus breaks when customElements.define is called out-of-order.

Is this situation considered out-of-scope for this proposal?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hydration is going to be component or library specific, and I'm not sure it will have a precise definition besides making the component "go".

For instance, in Lit's case hydration is fairly coarse grained, but per component. It means the component is fully initialized - the templates are ready to receive updates, event listeners are attached, and our deferred connectedCallback is called which can then fire events, etc.

I'm assuming that it only means connectedCallback, but you could imagine that a web component could have some logic in its constructor that assumes client-side rendering, which assumes top-down rendering, and thus breaks when customElements.define is called out-of-order.

Yes, absolutely this could happen. I don't think all components will be SSR and hydration compatible without adhering to some rules. I expect a lot of teams will control all their components, or use a base class that helps, or maybe we'll see utilities that proxy or late-register components that need it.

It's out of scope for this proposal in that this proposal is about an opt-in interoperable signal that the component should do whatever it deems appropriate at this point in time, and that's about the most I think we can do. It'll always be up to the components to choose what to do then.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification. It might be good to nail down some SHOULDs in this case (not necessarily MUSTs). E.g., thoughts that pop into my head are:

  • Should a component-author-author'ed (as opposed to framework-author-authored) connectedCallback fire in a deferred way?
  • What about framework-specific callbacks related to reactivity/rendering? E.g. in LWC we have renderedCallback for when the DOM is rendered, Lit has various callbacks like updated/firstUpdated/etc.
  • What if somehow a component is disconnected before the deferred connectedCallback code fires? Should disconnectedCallback fire before connectedCallback, or not at all?


// hydrate children
const deferredChildren =
this.shadowRoot.querySelectorAll('[defer-hydration]');
for (const child of deferredChildren) {
child.removeAttribute('defer-hydration');
}
}
}
```
4 changes: 2 additions & 2 deletions proposals/pending-task.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Author: Justin Fagnani

Status: Draft

Last update: 2021-066-20
Last update: 2021-06-20

# Background

Expand Down Expand Up @@ -166,4 +166,4 @@ Some use cases, like a progress bar that shows how much work is remaining, could

The `pending-task` event could carry a numeric work estimate property so that containers can estimate the total amount of pending work and incremental progress.

On the other hand, this may be better suited for ProgressEvent.
On the other hand, this may be better suited for ProgressEvent.