Skip to main content
Version: v5 (Preview)

SSR Output Target

The ssr output target builds a server-side rendering module for your component library. Given an HTML string or a document, it renders every Stencil component inside it and returns the resulting HTML. Shadow DOM components are rendered as Declarative Shadow DOM by default, and the browser hydrates the markup once your components' JavaScript loads.

The module has no Node.js dependencies, so it runs in Node.js 22+, Deno, Bun, Cloudflare Workers, and other runtimes with web-standard APIs. To render from a non-JavaScript server, see ssr-wasm. To decide between rendering on request and at build time, see Rendering Strategies.

stencil.config.ts
outputTargets: [
{
type: 'ssr',
},
];
note

Formerly dist-hydrate-script in Stencil v4.

To render every page of a site built with the www output target to HTML at build time, see Prerendering.

Output​

dist/ssr
├── index.js
└── index.d.ts

Expose the module to consumers through your package's exports:

package.json
{
"exports": {
"./ssr": {
"types": "./dist/ssr/index.d.ts",
"import": "./dist/ssr/index.js"
}
}
}

Rendering​

import { renderToString } from 'my-library/ssr';

const { html } = await renderToString(`
<html>
<body>
<my-button variant="primary">Save</my-button>
</body>
</html>
`);

The module exports three rendering functions:

FunctionReturnsUse
renderToString(html, options?)Promise<SsrResults>Render an HTML string or document and serialize it. The rendered HTML is in results.html.
streamToString(html, options?)ReadableStream<string>The same as renderToString, as a stream.
ssrDocument(doc, options?)Promise<SsrResults>Render into an existing document without serializing it (results.html is null). Use this when your server already has a Document and serializes it itself.

streamToString() emits strings. To use it as a web Response body, encode it first:

import { streamToString } from 'my-library/ssr';

export default {
async fetch(request: Request) {
const html = await loadPageTemplate(request);
const body = streamToString(html).pipeThrough(new TextEncoderStream());
return new Response(body, { headers: { 'content-type': 'text/html' } });
},
};

The module also exports setTagTransformer(), for when components are registered under transformed tag names on the client, so the server renders the same tags.

Using It in a Server Framework​

The ssr module renders Stencil components in any HTML string, so it works with any framework that lets you see a page's HTML on the server before it's sent. The browser then hydrates the rendered components, as long as the page also loads your components' client-side JavaScript, from loader-bundle or standalone.

Most frameworks let you run code on each response, usually as middleware or a response hook. Pass the page's HTML through renderToString() there:

import { renderToString } from 'my-library/ssr';

// the shape of this hook varies by framework
async function onResponse(response: Response): Promise<Response> {
if (!response.headers.get('content-type')?.includes('text/html')) {
return response;
}
const { html } = await renderToString(await response.text());
return new Response(html, response);
}

renderToString() needs the whole document, so run it on the complete response, not on streamed chunks.

Next.js​

React Server Components render each component separately, so there's no single page of HTML to pass through a hook. The React output target generates server versions of your wrappers instead: see React: Server-Side Rendering.

Writing SSR-Friendly Components​

Most components render on the server without changes: Stencil's server runtime implements the DOM APIs components normally use. The points below cover what does need care, for both server-side rendering and prerendering.

Browser-Only Code​

Code that needs a real browser, such as measuring layout or reading localStorage, should only run there. Check Build.isBrowser:

import { Build } from '@stencil/core';

connectedCallback() {
if (Build.isBrowser) {
this.width = this.el.getBoundingClientRect().width;
}
}

Build.isServer is the inverse. Both are resolved at build time: the browser build drops server-only branches, along with anything only they import, and the server build drops browser-only ones.

Async Data​

The server waits for a component's componentWillLoad() to finish before rendering it. Return a promise to render with fetched data:

async componentWillLoad() {
const response = await fetch('https://example.com/api/items');
this.items = await response.json();
}

Complex Props​

Server-rendered HTML can only pass props as attributes, which are strings. To give a component an object, array, or other complex value on the server, set it as a property in the beforeSsr hook:

await renderToString(html, {
beforeSsr(doc) {
doc.querySelector('my-menu').items = menuItems;
},
});

The browser doesn't receive a value set this way. For the component to keep it after hydration, serialize it to an attribute with @PropSerialize().

Style Size​

With Declarative Shadow DOM, each instance of a shadow DOM component carries its own copy of the component's styles. A component used many times on a page can add a lot of repeated CSS. To avoid this:

Components with encapsulation: { type: 'scoped' } or { type: 'none' } don't have this cost: their styles are always sent once.

Config​

dir​

default: dist/ssr

Where the module is written.

cjs​

default: false

Also write a CommonJS build, as index.cjs.

external​

default: []

Module IDs to leave as imports instead of bundling into the module. fs, path, and crypto are always external.

minify​

default: false

Minify the module.

empty​

default: true

Empty dir before each build.

skipInDev​

default: true, or false when the dev server runs with --ssr

Skip this output during development builds.

Render Options​

Every rendering function accepts these options:

OptionDescriptionDefault
serializeShadowRootHow shadow DOM components are rendered - see below.'declarative-shadow-dom'
staticComponentsTags to render on the server but never hydrate on the client. Useful for components with no client-side behavior, such as a footer.
excludeComponentsTags to skip entirely. They're treated like any unknown element, on the server and the client.
clientSsrAnnotationsInclude the comments and attributes the client uses to hydrate the rendered markup.true
urlSets location.href.
titleSets document.title.
languageSets the lang attribute on <html>.
directionSets the dir attribute on <html>.
canonicalUrlSets the href of the <link rel="canonical"> tag.
cookieSets document.cookie.
referrerSets document.referrer.
userAgentSets navigator.userAgent.
resourcesUrlThe base URL components use to resolve assets.'./'
removeScriptsRemove every <script> from the document.false
removeUnusedStylesRemove CSS not used by any element in the document.true
maxHydrateCountThe most components rendered on one page.300
timeoutMilliseconds to wait for rendering to finish before failing.15000
buildIdAdded as <html data-stencil-build="...">.random
runtimeLoggingPrint component console output to the server's console.false

renderToString() and streamToString() also accept options for the serialized HTML:

OptionDescriptionDefault
fullDocumentSerialize a complete document. Set to false to return only the rendered components.true
beforeSsr(document)Runs before rendering. Use it to set properties that can't be expressed as attributes.
afterSsr(document)Runs after rendering, before serialization.
prettyHtmlIndent the output.false
approximateLineWidthWrap lines at about this width, where the HTML allows it.no wrapping
removeHtmlCommentsRemove comments. Stencil's own hydration comments are kept.false
removeAttributeQuotesRemove quotes from attribute values where possible.false
removeBooleanAttributeQuotesWrite boolean attributes as hidden instead of hidden="".false
removeEmptyAttributesRemove empty class, dir, id, lang, name, and title attributes.true

beforeSsr is the way to pass complex data to a component on the server:

await renderToString(html, {
beforeSsr(doc) {
doc.querySelector('my-chart').data = new Map(/* ... */);
},
});

serializeShadowRoot​

Controls how encapsulation: { type: 'shadow' } components are rendered:

  • 'declarative-shadow-dom' - inside a <template shadowrootmode="open">, including the component's styles.
  • 'scoped' - as light DOM with scoped styles, written once to the document <head>. The client converts it to a real shadow root during hydration.
  • An object, to choose per component:
    serializeShadowRoot: {
    scoped: ['my-button'],
    default: 'declarative-shadow-dom',
    }
  • false - don't render shadow DOM components on the server.

Declarative Shadow DOM repeats a component's styles in every instance. For a component used many times on a page, 'scoped' sends its styles once.

Results​

renderToString() and ssrDocument() resolve to an object that includes:

PropertyContents
htmlThe rendered HTML (null from ssrDocument())
diagnosticsErrors and warnings from rendering
componentsEvery component rendered
hydratedCountHow many components were rendered
titleThe document's title
httpStatus200, or the value of a <meta http-equiv="status"> tag if the page has one - for example, set by a not-found component
anchors, imgs, scripts, stylesElements found in the rendered document