-
-
Notifications
You must be signed in to change notification settings - Fork 0
Hydration
The vite-plugin-vanjs plugin enhances VanJS's hydration capabilities. As explained in this discussion, we needed to identify ways to improve the very basic hydration implementation in VanJS so that the first page load is not influenced by any major layout shift.
Hydration applies to Server Side Rendering (SSR) enabled apps and provides a way to attach event listeners, state bindings and context to your server-rendered pages and has no effect on SPA apps since there is no HTML markup to work with.
VanJS usually doesn't re-render components or their elements as an effect of their reactivity, but the built in hydrate in VanJS usually targets a single server-rendered HTMLElement and replaces it with a client side rendered one, which is in effect a re-render. You are expected to know before hand which elements have events or state attached and then you need to create a special function to be executed on load. For a complex application this isn't very practical, or a great DX, but can be workable hydrating the entire app at the cost of some layout shifts.
To solve this issue, our plugin will break hydration in two distinct processes:
Let's look at how hydration works with a practical example. Consider this server-rendered HTML:
<!-- Hydration Target -->
<div id="app">
<!-- DOM Tree 1: Static Content -->
<div class="content">
<h1>Welcome</h1>
<p>This is static content</p>
</div>
<!-- DOM Tree 2: Interactive Application -->
<div class="interactive">
<div class="counter">
<button data-hk>Count: 0</button>
</div>
</div>
</div>
After hydration, the HTML becomes:
<!-- Hydration Target is now ready for subsequent updates -->
<div id="app" data-h> <!-- data-h attribute added -->
<!-- DOM Tree 1: Static Content - remains unchanged -->
<div class="content">
<h1>Welcome</h1>
<p>This is static content</p>
</div>
<!-- DOM Tree 2: Interactive Application - has been replaced -->
<div class="interactive">
<div class="counter">
<button data-hk>Count: 0</button>
</div>
</div>
</div>
Notes
- During the first page load, a server-rendered target is hydrated with event listeners and reactive bindings.
- A hydration target is now a root element like
<div id="app" />
instead of a particular button or form. - Interactive elements are given a special
data-hk
attribute to signal their need for state, context or events. - The
data-h
attribute is used to track hydrated targets, after the initial hydration, all targets get this attribute. - A very simple diffing algorithm matches the client-side rendered DOM (which is in essence a virtual DOM) with server-rendered DOM. The server-rendered target is compared with the client-rendered counterpart to identify DOM trees that need to be updated. This is to make sure we capture not only small pieces of state or events, but also any relevant application context.
- The diffing only applies to the initial hydration and implies checking for tagName, ID, class and children.length equality, then starts comparing children the same. There are cases when you need to force hydration to happen.
- The state of metadata tags is also initialized in this phase. This is to make sure that subsequent updates don't produce duplicate tags or other inconsistencies.
After navigating to /about
for instance, the HTML becomes:
<!-- Hydration Target is now fully refreshed -->
<div id="app" data-h>
<!-- DOM Tree 1: Static Content -->
<div class="content">
<h1>About</h1>
<p>Our story..</p>
</div>
<!-- DOM Tree 2: Static Content -->
<div class="related">
<h2>More sources</h2>
<p>Source 1</p>
<p>Source 2</p>
</div>
</div>
Notes:
- After initial hydration, updates replace children directly with no diffing, basically what VanJS does its own way.
- The
data-h
attribute helps distinguish between initial and subsequent hydrations. - New meta tags from new pages override the existing ones effortlessly.
The hydration process includes special handling for <head>
elements:
- Style/Link Tags
- Preserves existing styles to prevent FOUC (Flash of Unstyled Content)
- Manages stylesheets loading with proper fallbacks
- Handles both
<style>
and<link>
elements distinctly
- Meta Tags
- Non-style/link tags are processed separately
- Maintains document metadata without disrupting styles
The hydration system supports Promise-based content. More details soon.
The plugin creates proxies for both server and client environments to ensure consistent behavior:
// Server-side component with event
import van from '@vanjs/van'
function Button() {
const { button } = van.tags
// The proxy automatically adds data-hk
return button({ onclick: () => console.log("click") },
"Click me"
)
}
In this case, the <button />
is eligible to signal for hydration and is covered by the proxy to receive the data-hk
attribute.
In the following case you are expected to add the data-hk
attribute yourself:
// Manual data-hk for pure DOM events
function PureButton() {
const button = van.tags.button({
"data-hk": "", // manual attribute for DOM events
}, "Click me");
// this should happen on first hydration only
// to prevent re-adding the same event listener multiple times
if (typeof window !== "undefined") {
button.addEventListener("click", () => console.log("click"));
}
return button;
}
In other cases where van.derive
effects apply but no element is eligible to have the data-hk
attribute, you need to manually add the attribute yourself. This is because we don't have any other way to know what a particular DOM tree contains and how is expected to behave.
- Proxies wrap both
mini-van-plate
(server) andvanjs-core
(client) objects - The
data-hk
attribute is automatically added to elements with:- Event handlers (
onclick
,onmouseenter
, etc.) - State bindings
- Context dependencies
- Event handlers (
- Hydration can be enforced by adding the
data-hk
attribute where needed - Simple attribute-based hydration instead of complex keys
- Generally is not a good idea to use events or state attached to hydration targets