Hydrate server-rendered markup instead of rebuilding it - #21
Merged
Conversation
The client renderer can now adopt HTML that the SSR artifact already
produced, binding the existing DOM nodes to element ids rather than
creating new ones. `polymorph:dioxus` goes to 0.6.0: `run` takes a
`render-mode { fresh, hydrate }` and `operation` gains
`hydrate(list<element-id>)`.
The split follows dioxus's own, and lands on the guest/host boundary:
- Guest (`src/hydrate.rs`) walks its template tree and produces the
ordered element ids. It never looks at the DOM.
- Host (`applier.ts`'s `hydrate`) walks the DOM and binds them:
`data-node-hydration` attributes, `<!--node-id N-->text<!--#-->` and
`<!--placeholder N-->`.
Neither side compares HTML against the vdom. `dioxus-ssr`'s `pre_render`
numbers its markers with one monotonic counter, and the guest walk stops
at exactly those sites, so marker n and ids[n] are the nth stop of one
walk described twice. The initial `rebuild` still runs — that is what
assigns the ids — but through a `MutationWriter` with node-creating
operations suppressed.
Three divergences from dioxus-web, all deliberate:
Listeners flow as ordinary `new-event-listener` operations rather than
being parsed out of the marker's `,click:1` suffix. Ours is the only
form carrying the interned name id, and it is what makes the synthetic
`mounted` event and the observer-backed `resize`/`visible` families work
exactly as on a fresh mount. So there is no `to_mount` vector and no
special `onmounted` path.
The host validates: every marker index in range, each matched exactly
once, all matched. Upstream's unchecked `ids[parseInt(...)]` silently
binds `nodes[undefined]` on a stale marker. A mismatch is build skew, so
it throws through `onError` rather than falling back to a fresh render,
which would hide the skew and double the document.
The host collects comment nodes before processing them instead of
mutating the DOM during a TreeWalker, which is what makes upstream's
loop hard to follow. Marker indices are self-describing, so visit order
cannot matter.
`ssr`'s `render_to` now always pre-renders. Markers are what make the
markup adoptable and are inert otherwise; nothing wanted a marker-free
mode.
Gates, each adding one layer:
cargo test tests/hydration_order.rs — the walk emits one id per
marker across dioxus-ssr's own hydration corpus,
natively, with no DOM
deno task test hydrate_test.ts unit-tests the DOM walk;
hydrate_component_test.ts hydrates the real counter
component against the real prerendered golden
just e2e the same in Chromium, against a page whose markup
arrived server-rendered
Correspondence — that id n really is the node numbered n — only has
meaning where a real component's ids meet a real component's HTML, and
node identity is what proves it: hydration that quietly re-rendered
would pass every text assertion. Verified the check bites by permuting
two marker indices in the golden, which fails the component test.
Suspense, streaming hydration, server-data transport and serving a full
page (shell plus client bundle) from the wasi:http artifact remain out
of scope.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The client renderer can now adopt HTML that the SSR artifact already
produced — binding the DOM nodes that are already there to element ids
instead of creating new ones.
polymorph:dioxusgoes to 0.6.0:runtakes arender-mode { fresh, hydrate }andoperationgainshydrate(list<element-id>).The split
dioxus splits hydration in two, and the split lands on our guest/host
boundary almost exactly:
src/hydrate.rs(guest)applier.ts'shydrate(host)Neither side compares HTML against the vdom.
dioxus-ssr'spre_rendernumbers its markers with one monotonic counter, and the guest walk stops at
exactly those sites, so marker
nandids[n]are the nth stop of one walkdescribed twice. The initial
rebuildstill runs — that is what assigns theids — but through a
MutationWriterwith node-creating operationssuppressed.
Markers, for reference:
data-node-hydration="n[,event:bubbles]"onelements,
<!--node-id n-->text<!--#-->around dynamic text,<!--placeholder n-->.Three deliberate divergences from dioxus-web
Listeners flow as ordinary
new-event-listeneroperations, not parsedout of the marker's
,click:1suffix (which the host ignores). Ours is theonly form carrying the interned name id, and it is what makes the synthetic
mountedevent and the observer-backedresize/visiblefamilies workexactly as on a fresh mount. Consequently there is no
to_mountvector andno special
onmountedpath — both of which upstream needs.The host validates: every marker index in range, each matched exactly
once, all matched. Upstream's unchecked
ids[parseInt(...)]silently writesnodes[undefined]on a stale marker. A mismatch is build skew, so it throwsthrough
onErrorrather than falling back to a fresh render — a fallbackwould hide the skew and double the document.
The host collects comment nodes before processing them rather than
mutating the DOM during a TreeWalker, which is what makes upstream's loop
hard to follow. Marker indices are self-describing, so visit order cannot
affect correctness.
ssrnow always pre-rendersMarkers are what make the markup adoptable, and they are inert in a page
that never hydrates. Nothing wanted a marker-free mode, so there isn't one.
Gates
Each adds one layer, and the cheap ones sit at the bottom:
Correspondence — that id
nreally is the node numberedn— only hasmeaning where a real component's ids meet a real component's HTML, and
node identity is what proves it: hydration that quietly re-rendered
would pass every text assertion while failing every
assertStrictEquals.The browser lane does the same thing with a
data-server-renderedstampapplied before the module script boots.
Verified the check bites: permuting two marker indices in the golden makes
the component test fail (
waitFor timed out: count increments after click— clicking
#incdecremented). Restored afterwards.tests/hydration_order.rsis scoped to counts by design and says so;positional correctness is a DOM property and belongs to the host tests.
Out of scope
Suspense, streaming hydration, server-data transport, and serving a full
page (shell plus client bundle) from the
wasi:httpartifact. The servecomponent still returns the fragment only, so the hydration lane runs
through the SSG artifact and the existing harness.