Server-Side Rendering
Server-Side Rendering
Cosmos Components are built on top of the Web Components standard utilizing Shadow DOM. While Shadow DOM is great for isolating a component's DOM and styles, it comes with its own unique challenge for applications that pre-render pages on the server.
Since Shadow DOM and Web Components as a whole are a Browser API, they don't exist in a server environment. This means that, without any intervention, the components would arrive empty and without any functionality or styling on the client, resulting in a flash of unstyled content until they get hydrated on the client.
This means having to produce a light DOM version of the components on the server in order to reconstruct the Shadow DOM and scoped styles during the rendering process.
Stencil recently started leveraging Declarative Shadow DOM (DSD) to optimize the pre-rendering and hydration process of Cosmos web components. DSD allows the Shadow DOM structure to be serialized on the server and preserved as part of the document. This enables consistent component isolation and style scoping while improving performance and compatibility.
Usage
The @cosmos/web library includes a hydrate script that can be used in two different ways: hydrateDocument and renderToString.
renderToString
Allows to pass in a HTML string that also returns a promise of HydrateResults. The second parameter is a config object that can alter the output of the markup. The resulting string can be parsed from the html property.
import type { HydrateResults } from '@cosmos/web/hydrate';
import { renderToString } from '@cosmos/web/hydrate';
const results: HydrateResults = await renderToString(html, {
serializeShadowRoot: "scoped"
});
console.log(results.html);
Option | Type | Default | Description |
|---|---|---|---|
approximateLineWidth | number | | Determines when line breaks are set while serializing the component. |
prettyHtml | boolean | false | If set to true, prettifies the serialized HTML code, indents elements, and escapes text nodes. |
removeAttributeQuotes | boolean | false | If set to true, removes attribute quotes when possible, e.g., changes someAttribute="foo" to someAttribute=foo. |
removeEmptyAttributes | boolean | true | If set to true, removes attributes that don't have values, e.g., removes class="". |
removeHtmlComments | boolean | false | If set to true, removes abundant HTML comments. Stencil still inserts hydration comments for reconciliation. |
beforeHydrate | (document: Document, url: URL) => void | Promise | | Allows modification of the document and its components before the hydration process starts. |
afterHydrate | (document: Document, url: URL, results: PrerenderUrlResults) => void | Promise | | Allows modification of the document and its components after the virtual DOM rendering but before the serialization process starts. |
serializeShadowRoot | "scoped" | "declarative-shadow-dom" | declarative-shadow-dom | Configures how the components should be rendered on the server, e.g. as Declarative Shadow DOM, as scoped components or as a mixture of both. |
hydrateDocument
It is also possible to use hydrateDocument as a part of your server's response logic before serving the web page.hydrateDocument takes two arguments, a document, and an options object. The function returns a promise with the hydrated results.
We now also expose the createWindowFromHtml method which allows you to create a node friendly DOM implementation, allowing you to manipulate and render on the server. This can be used instead of other JavaScript implementation of web-browser, such as happy-dom and domino
// server
import { hydrateDocument, createWindowFromHtml } from "@cosmos/web/hydrate";
function hydrate(template: string) {
const win = createWindowFromHtml(template, 'unique-id'));
const document = win.document
const result = await hydrateDocument(document, {
// hydrateDocument config options
})
// Result is just for diagnostic, `document` is transformed in place.
if (result.diagnostics.length > 0) {
result.diagnostics.forEach((diagnostic) => {
console.error({ message: 'hydration failed', diagnostic });
});
}
// Serialize
return `<!doctype html> ${document.documentElement.outerHTML}`;
}
hydrate(/*html*/)Option | Type | Description |
|---|---|---|
canonicalUrl | string | Sets the canonical URL for the document. |
constrainTimeouts | boolean | Constrains setTimeout() and setInterval() to 1ms. Defaults to true. |
clientHydrateAnnotations | boolean | Includes HTML comments and attributes for client-side hydration. Defaults to true. |
cookie | string | Sets document.cookie. |
direction | string | Sets the dir attribute for text direction on the <html> element. |
language | string | Sets the lang attribute on the <html> element. |
maxHydrateCount | number | Specifies the maximum number of components to hydrate. Defaults to 300. |
referrer | string | Sets document.referrer. |
removeScripts | boolean | Removes <script> elements from the document. Defaults to false. |
removeUnusedStyles | boolean | Removes unused CSS from the document. Defaults to true. |
resourcesUrl | string | Specifies the base URL for loading resources such as assets. |
timeout | number | The time in milliseconds to wait for hydration before a timeout error occurs. Defaults to 15000. |
title | string | Sets document.title. |
url | string | Sets location.href for the document. |
userAgent | string | Sets navigator.userAgent. |
serializeShadowRoot | "scoped" | "declarative-shadow-dom" | Configures how the components should be rendered on the server, e.g. as Declarative Shadow DOM, as scoped components or as a mixture of both. |
DSD improves the balance between Shadow DOM isolation and server-side rendering by allowing encapsulated components to be serialized and hydrated seamlessly. By setting serializeShadowRoot to declarative-shadow-dom, this behavior can be enabled since @cosmos/[email protected] but is experimental and not recommended for production use.
⚠️ Important Considerations
The experimental Declarative Shadow DOM support has known limitations that prevent production use:
- Server-side hydration takes significantly longer to complete
- The current DSD standard implementation duplicates styles for each component instance (e.g., multiple cosmos-text components will inject duplicate styles), resulting in substantially increased response sizes
- Overall performance may be negatively impacted in component-heavy applications.
We will continue monitoring developments in this area and provide updates as the standard matures and these limitations are addressed.