Hydration
Hydration is not a diff. Sigil walks the server's existing DOM, claims the nodes it needs, and attaches update functions to them. The HTML the server sent stays exactly as it is; the browser never re-creates the markup it is already looking at.
The alternative — throwing the server's HTML away and re-rendering — is what makes frameworks flash on first load. There is no flash here because the first paint is the real page.
What the server sends
Three things, all before the browser's JavaScript runs:
- The rendered HTML — the real page, not a skeleton.
__grimoire_state__— a<script type="application/json">holding theloaddata, keyed by route pattern.__grimoire_env__— thePUBLIC_variables, for$env/dynamic/publicin the browser.
<script id="__grimoire_state__" type="application/json">{…}</script>
<script>window.__grimoire_env__ = {…}</script>
<script type="module" src="/__grimoire__/hydrate.js"></script>Because the state is a JSON script tag rather than an inline assignment, the data is inert until it is read.
Claiming
The runtime's job is to take the nodes that are already there:
isClaimed(el)— has this node already been taken? Prevents two components claiming the same node.claim(...)— take a node, or create one if none matches.claimText(nodes)/claimComment(...)— the same for text and comment nodes.hydrateKeyedList(...)— match a list of children to their keys.insert(...)— add a node where there is nothing to claim.
This is why key matters so much. Keyed hydration matches an existing row to its book by key, so a reordered list moves the nodes the server already sent. With an index as the key, hydration cannot tell which row is which, and it has to rebuild.
Deferred load values
A load that returns a promise for a top-level value has already been awaited by the time the HTML is sent — but a value that arrives later is patched in by a small inline script that updates __grimoire_state__ and dispatches a grimoire:deferred event. The client listens for that event and patches the affected nodes.
That is the mechanism behind the streaming example in load functions: a page renders immediately, and the slow part fills in without a second request.
Error pages skip hydration
A +error.tsx gets no __grimoire_state__ and no hydrate script. It is server-rendered only, because it is not in the client route table and there would be nothing to hydrate against. It still comes wrapped in the layout tree — see The server.
Mismatches
If the server's HTML and the client's expectations disagree, the node is replaced. This is rare, and in development it usually means one of:
- Different
Date.now(),Math.random()orcrypto.randomUUID()on each side. Anything nondeterministic in render output will mismatch by construction — generate ids in an effect, or pass them throughload. - A component reading
windoworlocalStorageduring render. There is no window on the server, so the value differs. Read them in an effect. - Conditional rendering on a signal read outside a tracked context.
In production the same mismatch is repaired silently rather than warned about, so a page that flickers only in dev is almost always one of the three above.