Why SSR matters for web components
Web components rendered purely on the client produce an empty custom element tag in the initial HTML. The browser must download, parse, and execute JavaScript before the component’s shadow DOM appears. This creates:- Slower first paint: Users see unstyled or blank content until JS loads.
- Poor SEO: Crawlers that do not execute JavaScript see no component content.
- Layout shifts: Components appear progressively as scripts execute.
How @lit-labs/ssr works
@lit-labs/ssr is a Node.js package that renders Lit templates and LitElement components to HTML strings or streams. It works without a real browser by using a minimal DOM shim — just enough to register and render custom elements.
The rendering pipeline:
- Your server imports Lit component definitions into Node.js (the DOM shim provides
customElements, a minimalHTMLElement, etc.). - You call
renderThunked()(orrender()) with a lit-html template result. - The function returns a
ThunkedRenderResult— an array of strings and lazy thunks — which is consumed to produce HTML. - The HTML is sent to the browser.
LitElementshadow roots appear inside<template shadowrootmode="open">tags (Declarative Shadow DOM). - On the client, the browser parses DSD natively (or via polyfill) and attaches shadow roots before JavaScript runs.
@lit-labs/ssr-clienthydrates the existing DOM so Lit can update it efficiently.
@lit-labs/ssr is part of Lit Labs and is pre-release software. The API may receive breaking changes before stabilizing.Declarative Shadow DOM
Declarative Shadow DOM (DSD) is the browser feature that makes SSR of web components possible. Instead of callingattachShadow() in JavaScript, the shadow root is declared directly in HTML:
<template> element — no JavaScript required. @lit-labs/ssr emits this format automatically for every LitElement.
DSD is supported natively in all modern browsers. For older browsers you can use the @webcomponents/template-shadowroot polyfill.
Server-only templates
@lit-labs/ssr also exports its own html tagged template literal for server-only templates. These templates:
- Can render full documents including
<!DOCTYPE html>. - Can render into elements Lit normally cannot (e.g.,
<title>,<textarea>,<script type="text/json">). - Omit the hydration marker comments, making the output slightly smaller.
- Cannot be hydrated or rendered on the client.
Supported environments
@lit-labs/ssr targets Node.js (14+). It can run in two modes:
Node.js global scope
Import component definitions directly into the Node.js process. The DOM shim is installed on the global object automatically. Simplest setup for most servers.
VM context
Load component modules into an isolated VM context using
renderModule(). Each request gets a fresh global, preventing state leakage between requests. Requires --experimental-vm-modules.Streaming
renderThunked() returns a lazy ThunkedRenderResult that produces HTML incrementally. You can stream this directly to the HTTP response using RenderResultReadable, a Node.js Readable stream:
Available integration packages
@lit-labs/ssr
Core SSR package. Renders Lit templates and LitElement components to HTML in Node.js.
@lit-labs/ssr-client
Client-side hydration support. Re-associates server-rendered DOM with Lit’s reactive system.
@lit-labs/nextjs
Next.js plugin for deep server rendering of Lit components. Wraps
@lit-labs/ssr-react and handles the webpack configuration automatically.@lit-labs/eleventy-plugin-lit
Eleventy plugin that pre-renders Lit components at build time, with optional hydration.