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/index.d.ts", "svelte": "./dist/client-svelte/index.js", "default": "./dist/client/index.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. They carry the same
version-compatibility limitation described in
Svelte Conditional Exports.
See Build 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
— you don’t need to configure that 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],});Both directions are covered by end-to-end tests in the repository.
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.