Hydration
With Static Generation or Server rendering, the browser receives real HTML on first load. Hydration is what happens next: the client adopts that markup in place — claims the nodes, wires signals and event handlers onto them, and never tears the tree down to reconcile a virtual DOM.
A plain CSR esdev build skips this: #app starts empty and the client builds every route from scratch. Hydration reuses supported server-rendered nodes and attaches their client behavior; its cost depends on the page and the bindings involved.
When hydration runs
| Situation | Client behavior |
|---|---|
| First load of an SSG page (a configured prerender build) | Adopt the pre-rendered HTML |
| First load of an SSR page (an app server or adapter) | Adopt the per-request HTML |
Client navigation after first paint (<Link>) | Build the next route on the client (CSR) |
| Plain CSR build, first visit | Build into an empty #app (no adopt) |
Direct URL hits, refresh, and open-in-new-tab always get a full document — SSG/SSR HTML hydrates on that first paint. SPA navigation is layered on top as a progressive enhancement.
When the structure matches, hydration attaches bindings to the existing nodes. Unsupported shapes or mismatches can fall back to client rendering, as described below.
How to enable it
Use plugin mode: "ssg" with a prerender target for static HTML, or mode: "ssr" with an app-owned server entry for request rendering. Both modes compile browser release modules for hydration. Server HTML must include the sentinel and island/data payloads that the client expects; the plugin does not produce that request server.
esdev build --minify
Plugin mode: "spa" emits CSR output for a client-only app. See Static Generation and Server for the required entries.
The app shell stamps a sentinel on the mount root when hydration is expected:
<div id="app" data-afw-hydrate>…server-rendered children…</div>
mountApp sees data-afw-hydrate, threads a DOM cursor through the layout chain, and calls each route's hydrateAt factory instead of replaceChildren() + build. When the sentinel is absent, the client mounts with a normal CSR build.
What hydrates today
Fine-grained adoption is implemented for the constructs you use in real apps:
Pages and nested layouts — one cursor walks the layout chain; each layout adopts its slot and hands the cursor to the nested route.
Keyed lists —
{items.map(…)}adopts each item root, then reconciles updates in place.Conditionals —
{cond ? <A/> : <B/>}and{cond && <X/>}adopt the rendered branch and swap branches reactively after hydration.Component
{children}slots — slotted content is adopted by the composing parent; context and reactivity follow the logical tree.Built-ins —
<Link>,<Portal>,<ContextProvider>, and<ErrorBoundary>participate in first-paint adoption (portals defer relocation until hydration finishes).Rich island props — JSON-safe props serialize into a
<script id="__afw_h">payload so components upgrade with the correct object/array values, not attribute round-trips.Loader output — inline loader data in the HTML keeps
router.dataaligned on first paint; see Data Fetching.
Hydration assumes the client would render the same structure the server sent. Avoid non-deterministic output during render (Date.now(), Math.random(), browser-only reads). Put time-varying or per-user data in loaders or fetch after mount. For client-only UI branches, gate on onMount or a post-hydration signal so SSG does not emit a different tree than the client.
SPA navigation after hydration
After first paint the hydration flag clears. <Link> navigations load a route chunk and build the next view with the same CSR path a pure client app uses. Components compiled for hydration are dual: adopt on first paint, build on subsequent navigations — one bundle, both behaviors.
For a full page load instead of SPA navigation, use a normal <a href> or configure MPA-style routing when you want every navigation to hit the server.
Mismatches and recovery
If the client cannot adopt what the server sent — wrong tag, missing marker, divergent structure — the runtime reports a hydration mismatch through its error-reporting API and rebuilds that component via CSR. The rest of the page stays adopted; recovery is per component, not a whole-page bail-out.
Routes or layouts the compiler cannot yet adopt fall back to a clean CSR build for that subtree (the app still works; you may see a brief flash). Multi-root fragment route outputs are the main remaining gap on the adopt path.
Compared to other frameworks
AFW is zero-VDOM with fine-grained signals. Hydration claims existing nodes and binds only what is reactive — the same shape as Solid or Svelte 5, expressed on Custom Elements. There is no tree-wide reconcile pass.
Related
Static Generation — pre-render routes at build time.
Server — per-request SSR with an app server or adapters.
Data Fetching — loaders and
router.dataacross SSG, SSR, and SPA nav.Portal — relocation deferred until hydration completes.
Navigation —
<Link>and client-side routing after first paint.