SSR
Browsers know how to instantiate custom elements. A server renderer usually sees an unknown HTML tag and has no component implementation to invoke.
svebcomponents bridges that gap with a server-specific renderer for each
custom element. The design follows Lit’s ElementRenderer model and serializes
the component’s shadow root using declarative shadow DOM.
Package-author Flow
Section titled “Package-author Flow”A component package exposes its browser entrypoint and a matching SSR entrypoint:
{ "exports": { ".": { "types": "./dist/client/ExampleComponent.d.ts", "svelte": "./dist/client-svelte/ExampleComponent.js", "default": "./dist/client/ExampleComponent.js" }, "./ssr": { "types": "./dist/server/ssr.d.ts", "svelte": "./dist/server-svelte/ssr.js", "default": "./dist/server/ssr.js" } }}The browser entrypoint defines the custom element. During the build, the SSR
entrypoint is generated as an ElementRenderer subclass that knows how to
render the Svelte component on the server.
The optional svelte conditions create renderer and browser builds that share
the consuming application’s Svelte runtime, with the
version-compatibility tradeoff
that carries.
See Publishing your package
for how matching /ssr exports enable this output.
App-author Flow
Section titled “App-author Flow”The consuming application needs two pieces of setup.
First, add the svebcomponents transform before the Svelte plugin:
import { sveltekit } from "@sveltejs/kit/vite";import svebcomponents from "@svebcomponents/ssr/vite";import { defineConfig } from "vite";
export default defineConfig({ plugins: [svebcomponents(), sveltekit()],});The plugin automatically adds @svebcomponents/ssr to Vite’s ssr.noExternal
so you don’t need to configure it yourself. If your own component package
also ships a raw-.svelte export (e.g. an externalSvelte build sharing
this app’s Svelte runtime), add it alongside via the plugin’s noExternal
option instead of editing vite.config.ts’s ssr.noExternal separately:
svebcomponents({ noExternal: ["my-component-package"] });Then load the component’s server renderer once, e.g. from hooks.server.ts:
import "my-component-package/ssr";The generated renderer knows its own tag name (read from the component’s
<svelte:options customElement> declaration at build time) and registers
itself with ElementRendererRegistry as soon as it’s imported. The DOM shim
is guaranteed to install first regardless of import order or how a bundler
chunks things.
Any Custom Element, Not Just Svelte-built Ones
Section titled “Any Custom Element, Not Just Svelte-built Ones”Nothing in the SSR pipeline is specific to how a custom element is
implemented. Rendering goes through Lit’s ElementRenderer contract and
nothing else, so any element with a conforming renderer can be server-rendered
, including Lit elements:
import { LitElementRenderer } from "@lit-labs/ssr/lib/lit-element-renderer.js";import { ElementRendererRegistry } from "@svebcomponents/ssr";
ElementRendererRegistry.use(LitElementRenderer);use() registers a renderer that selects the elements it serves itself,
through Lit’s static matchesClass hook, rather than being bound to one tag.
That single line therefore covers every Lit element in the app. An explicit
ElementRendererRegistry.set() registration still wins, so one specific
element can be given a different renderer.
The reverse direction works too: the renderers svebcomponents generates
implement matchesClass, so they can be handed straight to @lit-labs/ssr’s
own render():
import { render } from "@lit-labs/ssr";import MyComponentRenderer from "my-component-package/ssr";
render(html`<my-component title="Hello"></my-component>`, { elementRenderers: [MyComponentRenderer],});What Makes a Component Asynchronous
Section titled “What Makes a Component Asynchronous”Whether an element renders synchronously decides which host integrations can server-render it. There are exactly two ways onto the async path:
- The component awaits while rendering — an
awaitin its instance script, or anywhere Svelte’sexperimental.asyncallows one. - Its preparation hook returns a promise — the adjacent
<entry>.ssr.tsmodule described in server preparation module, whose default-exportedSsrPreparefunction may return a promise.
Everything else renders synchronously, including a hook that returns nothing. A hook that bails out early, because the host already supplied the value it would have fetched, keeps the synchronous path available, which is what makes the second form safe to add to a component that hosts may render either way.
Why a preparation hook rather than just awaiting
Section titled “Why a preparation hook rather than just awaiting”The two are not interchangeable, and the hook exists for the cases awaiting cannot cover:
- Server-only dependencies stay off the client.
<entry>.ssr.tsis compiled into the server output only. A database client, an API SDK or a secret-bearing module used there never enters the browser bundle. Anawaitin the component puts that code in both. - The result survives hydration. Properties set through the hook’s
setPropertyjoin rich-property serialization, so the client adopts the prepared value instead of fetching it again. A value awaited inside the component is local state: it is not serialized, and the browser repeats the work.
Awaiting in the component is the right tool when the asynchrony is part of rendering. The hook is the right tool when the component owns acquiring its own data.
Async Rendering Outside Svelte
Section titled “Async Rendering Outside Svelte”Svelte gates asynchronous server rendering behind a module-global flag, which
is normally set for you by a Svelte app compiled with
compilerOptions.experimental.async.
A host application that is not a Svelte app has no such compilation, and a
component package’s own server bundle cannot set the flag either: it carries
its own copy of Svelte, while render() is called through the copy
@svebcomponents/ssr imports. Such an app must therefore opt in explicitly on
the server:
import "@svebcomponents/ssr/enable-async";Without it, render() runs synchronously even when awaited, and a component
that performs asynchronous work throws await_invalid. This is only needed
outside Svelte hosts; a SvelteKit app compiled with experimental.async
already has it.
What Happens During Rendering
Section titled “What Happens During Rendering”On the server, the Vite plugin finds custom element tags in Svelte templates and rewrites them to a wrapper component. The wrapper:
- Looks up the custom element and its registered renderer.
- Passes attributes and properties to that renderer.
- Emits the rendered shadow root as declarative shadow DOM.
On the client, the wrapper renders the original custom element tag so the browser can upgrade it normally. When the element upgrades, the server-rendered shadow DOM is hydrated: adopted in place rather than wiped and re-rendered.
The examples above use Svelte. Nothing about the element is Svelte-specific; only the host application’s wiring is. For Vue, and for building an integration against another framework, see Framework Integrations.
For runtime APIs, direct package usage, transform details, and the complete
list of limitations, see the
@svebcomponents/ssr reference.