Server RenderingHow server rendering and hydration work in Semantic UIserverAPI Reference
Categories

Server Rendering

Server rendering turns a component into an HTML string and wraps its shadow content in Declarative Shadow DOM, a <template shadowrootmode="open"> element that the HTML parser converts into a real shadow root without running any JavaScript.

A web component is otherwise empty markup until its script loads, defines the element, and mounts it. Delivered this way it is on screen at first paint, and the only work left for the client is attaching reactivity to DOM that already exists.

Usage Guide See the server rendering guide for setting server rendering up in a project.

The Pipeline

renderToString() HTML string with markers, wrapped in <template shadowrootmode="open">
|
v
browser parse shadow root created, content visible, no JavaScript yet
|
v
element upgrade custom element defined, existing shadow root detected
|
v
hydrate() reactions wired to the existing DOM

The definition is the same on both sides. createComponent runs on the server and again on the client, so a component is written once and the environment decides what happens.

Server Rendering

ServerRenderer walks the same AST as the client renderer and shares its expression evaluator, but produces a string. Nothing it emits is reactive, because nothing on the server changes after it is sent.

It stamps a marker at each dynamic position so the client can find it later.

Position Server output
Text expression <!--sui:v1:3-->Hello
Block open <!--sui-block:v1:5-->
Block close <!--/sui-block:v1:5:b1000-->
Attribute expression evaluated inline, no marker

Block close markers carry which branch was taken, so the client knows what the server chose without re-evaluating the condition.

Two behaviours follow from having no reactivity. {#async} renders its loading branch and never awaits the promise, because the server has no way to update what it already sent. Timers and listeners registered through the callback params are no-ops.

Nested components expand recursively. The renderer scans its own output for custom element tags, looks each one up in the registry, and renders it as nested Declarative Shadow DOM.

Static Markup

renderToStaticMarkup renders the same definition for a destination that never runs its JavaScript. The renderer makes the same walk with its markers off, so the result is the template’s own markup alone. The <template shadowrootmode> wrapper, the <style> and the hydration comments and bind attributes of the web form are all absent, and a mail client, a feed reader or a static page holds it as delivered.

import { renderToStaticMarkup } from '@semantic-ui/component/server';
const html = renderToStaticMarkup(Card, { title: 'Hello' });
// <div class="card">Hello</div>

Slots fill in place from the slots option, there being no light DOM to project. A nested component renders where its tag sat, with its children assigned to its slots by their slot attribute. A definition without a tagName renders the same way, so a mail or a static page needs no custom element.

Both functions are exported from @semantic-ui/component/server, the package’s server entry, which is also what gives the native engine its server renderer. The root entry is the browser’s and carries neither.

text: true renders plain text, where nothing is escaped and {#html} is raw either way. css: true returns { html, css }, the css being that of every component the render reached, parent first, for a mail inliner or a page’s <style>.

Declarative Shadow DOM

The parser attaches the shadow root, moves the content into it, and drops the template element.

Component CSS travels inside that template as a <style> tag, since adoptedStyleSheets cannot cross the network. Hydration removes the tag once the stylesheet has been adopted.

Hydration

Hydration trusts the server. It walks the markers and wires a reaction to each position, without re-rendering or checking the result against what the client would have produced.

The first run of every wired reaction evaluates but does not write:

if (comp.firstRun && hydrating) {
compute(state); // read the signals, registering dependencies
return; // leave the DOM alone
}

Reading a signal inside a running reaction is the only way to register a dependency on it, so the evaluation cannot be skipped. The write can be, because the server already produced that value. Every run after the first writes normally.

Markers are removed after wiring completes. The reactions hold references to real text nodes and elements rather than to the comments, so nothing depends on them once binding is done.

Version Mismatch

Markers carry a version. Before hydrating, the client reads the first marker it finds and compares it against the version it was built with.

On a mismatch it discards the server content and renders from scratch. A stale payload costs one render, rather than producing a tree whose bindings point at positions that have since moved.

An element carrying an ssr attribute is skipped entirely. Its markup stays as delivered and no JavaScript claims it.

Previous
Native Renderer
Next
Lit Renderer