Hydration
Svelte’s compiler-generated custom elements cannot hydrate on their own: when an element upgrades, its constructor attaches a shadow root (which, per spec, clears any declarative shadow root the server delivered) and then mounts the component from scratch. Server-rendered DOM was paint-and-throw-away — good for first paint and SEO, but every node was re-created the moment JavaScript arrived.
Components built with @svebcomponents/build hydrate instead: the
server-rendered shadow DOM is adopted in place. The existing nodes are
kept, component styles are reused rather than re-injected, and the component
is fully reactive afterwards.
Hydration is on by default for every SSR-enabled component. There is nothing to configure — but it helps to know how it works and where its current limits are.
How It Works
Section titled “How It Works”Everything is built on documented Svelte extension points, not forked compiler output:
- During the build, auto-options injects
Svelte’s official
customElement.extendoption, wiring in a wrapper class from@svebcomponents/ssr/hydration. - The wrapper intercepts the
attachShadowcall in the generated element’s constructor and claims the declarative shadow root before it would be cleared. - On connect, the wrapper hydrates the claimed shadow root with Svelte’s
public
hydrate()API instead of mounting. - To guarantee the server markup structurally matches what the client
expects, both sides render the component through the same internal
HydrationHostcomponent: the generated SSR entry uses a server-compiled copy, the client hydrates against it. - The same symmetry principle protects the element inside hydrating
Svelte host apps (e.g. SvelteKit): the SSR wrapper and the browser
wrapper render structurally identical
<svelte:element>fragments (selected per environment via export conditions rather than a runtime branch), so the host’s hydration claims the server-rendered element instead of re-creating it — which would otherwise discard the shadow root before the element could hydrate it. - Rich (non-attribute) props the server rendered with are serialized into the shadow output and ported back before hydrating, so host frameworks that only re-supply props after their own asynchronous hydration don’t cause a mismatch.
Graceful Degradation
Section titled “Graceful Degradation”Anything the wrapper cannot hydrate falls back to Svelte’s untouched mount path — the exact behavior all custom elements had before hydration existed:
- No declarative shadow root (the element was never server-rendered): plain client-side mount.
- Components that declare slots: see limitations below.
- Re-connection after teardown (the element was removed long enough for Svelte to destroy the component, then re-inserted): fresh mount.
- Hydration mismatches: Svelte’s own recovery logs a warning, clears the shadow root, and re-mounts.
In every case the worst outcome is a re-render — never a broken component.
Opting Out
Section titled “Opting Out”Per package, in svebcomponents.config.ts:
import { defineConfig } from "@svebcomponents/build";
export default defineConfig({ hydratable: false,});Per component: declaring your own extend in
<svelte:options customElement={{ ... }}> is always respected —
auto-options will not inject over it (your component then keeps Svelte’s
default mount behavior unless your extension implements hydration itself).
Limitations
Section titled “Limitations”Components that declare <slot> elements currently fall back to the mount
path — they render correctly, but re-render on upgrade instead of adopting
the server DOM. In dev mode the element logs a console.info when this
happens, so the fallback is never mistaken for broken hydration.
The reason is historical: Svelte 5 still compiles <slot> elements in
custom-element mode through a legacy transformation that creates slot
elements imperatively, without hydration awareness. That transformation is
expected to disappear with Svelte 6, at which point literal <slot>
elements flow through Svelte’s normal — hydration-aware — element path, and
this limitation should lift with little or no change on our side. We chose
not to build elaborate slot-claiming machinery on top of a compilation path
that is already scheduled for removal.
Legacy component events
Section titled “Legacy component events”createEventDispatcher events are not forwarded through the hydration host.
Events dispatched via $host() —
the idiomatic pattern for custom elements — bubble natively and work
unchanged.
Component exports
Section titled “Component exports”exported component functions are not exposed as element methods on
hydrated hosts.
Bundle Cost
Section titled “Bundle Cost”Hydration support adds roughly 4–5 KB gzipped to a self-contained
client bundle (Svelte’s hydration internals plus the wrapper). Builds using
Svelte conditional exports
share the host application’s Svelte runtime, where the overhead is smaller.
If a component is only ever used client-side, hydratable: false reclaims
it.