Server-Side Rendering
One of the core benefits of FAST's declarative HTML templates is stack-agnostic server-side rendering. Because <f-template> markup is standard HTML with binding expressions — not JavaScript — any server technology can render the initial page without a JS runtime.
The Rendering Contract
A server-side renderer must produce HTML that follows these conventions so the client-side FAST runtime can hydrate it:
- Declarative Shadow DOM — Custom element content is rendered inside a
<template shadowrootmode="open">element, which the browser expands into a shadow root. - Hydration markers — Attribute bindings are annotated with
data-fe="N"attributes on elements (whereNis the count of attribute bindings on that element). Content bindings are wrapped in<!--fe:b-->VALUE<!--fe:/b-->comment markers. - State resolution — Binding expressions like
{{title}}are resolved to their initial values from a state object.
Example: Given this template and state:
<f-template name="greeting-card">
<template>
<h2>{{title}}</h2>
<button ?disabled="{{!isActive}}" @click="{handleClick($e)}">
{{buttonLabel}}
</button>
</template>
</f-template>
{ "title": "Hello", "isActive": true, "buttonLabel": "Click me" }
A server renderer produces:
<greeting-card title="Hello" is-active button-label="Click me">
<template shadowrootmode="open">
<h2><!--fe:b-->Hello<!--fe:/b--></h2>
<button data-fe="2">
<!--fe:b-->Click me<!--fe:/b-->
</button>
</template>
</greeting-card>
Note that data-fe="2" appears on the <button> because it has two attribute bindings: the boolean ?disabled binding and the @click event binding. Content bindings ({{title}} and {{buttonLabel}}) use comment markers instead. Event bindings and attribute directives are client-only — the server strips them but allocates binding slots for hydration.
When the page loads, the FAST declarative runtime finds these markers and attaches reactive bindings to the existing DOM nodes instead of re-rendering.
Hydration Flow
The end-to-end flow from server to interactive page follows these steps:
- Server renders — The renderer resolves
{{bindings}}against the state, injects Declarative Shadow DOM<template>elements, and adds hydration markers. - Browser loads HTML — The browser parses the page. Declarative Shadow DOM
<template>elements are automatically expanded into shadow roots. - JavaScript loads — Component classes are defined with
template: declarativeTemplate(), which waits for matching<f-template>elements and resolves the template. - Template resolution —
declarativeTemplate()coordinates with the<f-template>elements to parse the declarative markup and supply the compiled template to each element definition. - Hydration — If
enableHydration()was called before FAST elements connect, FAST detects the pre-rendered shadow DOM, maps existing DOM nodes to binding slots using hydration markers, and re-establishes reactive observations. WithoutenableHydration(), the element renders client-side instead. - Interactive — The page is fully interactive. Property changes trigger targeted DOM updates.
The controller exposes two properties: isPrerendered resolves true when the element had a declarative shadow root at connect time, while isHydrated resolves true only when hydration actually ran successfully. Use these to detect how the element was rendered and when it is fully interactive.
State Propagation
When the server renders custom elements, attribute values on the host element become the initial state for template bindings inside the shadow DOM.
<greeting-card title="Hello" message="Welcome"></greeting-card>
The title and message attributes become state properties that resolve {{title}} and {{message}} in the element's template.
Nested Custom Elements
State flows from parent to child elements. Unbound state keys — values from the parent state that the parent template does not consume — propagate automatically to child custom elements.
<!-- Parent state: { "theme": "dark", "username": "Jane" } -->
<app-header username="{{username}}">
<!-- theme propagates to user-avatar even though
app-header's template doesn't use it -->
<template shadowrootmode="open">
<user-avatar username="{{username}}"></user-avatar>
</template>
</app-header>
Special Attribute Handling
The renderer applies special rules for certain HTML attributes:
| Attribute pattern | State property | Example |
|---|---|---|
data-* |
dataset.* (camelCase) |
data-user-id → dataset.userId |
aria-* |
camelCase ARIA property | aria-label → ariaLabel |
Boolean (?attr) |
Truthy/falsy evaluation | ?disabled="{{isOff}}" → present or absent |
Using @microsoft/fast-build
The @microsoft/fast-build package is a build-time renderer for declarative templates, powered by a WebAssembly core. It implements the rendering contract described above and is primarily used in testing and development workflows.
For production server-side rendering, consider the @microsoft/webui project, which provides a full rendering pipeline.
Installation
npm install --save @microsoft/fast-build
CLI Usage
The fast build command renders an entry HTML file with a JSON state file:
fast build --entry=index.html --state=state.json --output=output.html --templates="./components/**/*.html"
| Option | Default | Description |
|---|---|---|
--entry |
index.html |
Entry HTML template to render |
--state |
state.json |
JSON file containing the initial state |
--output |
output.html |
Where to write the rendered HTML |
--templates |
(none) | Glob pattern(s) for <f-template> HTML files |
--attribute-name-strategy |
camelCase |
How to map attribute names to properties |
--config |
fast-build.config.json |
Path to a configuration file |
Example
entry.html:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>My App</title></head>
<body>
<greeting-card title="{{title}}" message="{{message}}"></greeting-card>
<script type="module" src="./main.ts"></script>
</body>
</html>
state.json:
{
"title": "Hello FAST",
"message": "Server-rendered with WebAssembly"
}
templates.html:
<f-template name="greeting-card">
<template>
<h2>{{title}}</h2>
<p>{{message}}</p>
</template>
</f-template>
Run the build:
fast build \
--entry=entry.html \
--state=state.json \
--output=index.html \
--templates=templates.html
The output (index.html) contains the fully resolved HTML with Declarative Shadow DOM and hydration markers.
Configuration File
Instead of passing every option on the command line, create a fast-build.config.json:
{
"entry": "entry.html",
"state": "state.json",
"output": "index.html",
"templates": "./components/**/*.html"
}
The CLI automatically loads fast-build.config.json from the current directory when it exists. CLI arguments always take precedence over config file values. File paths in the config are resolved relative to the config file's directory.
Attribute Name Strategy
The --attribute-name-strategy option controls how HTML attribute names on custom elements map to state property names:
| Strategy | Behavior | Example |
|---|---|---|
camelCase (default) |
Dashes converted to camelCase | foo-bar → {{fooBar}} |
none |
Dashes preserved | foo-bar → {{foo-bar}} |
fast build --attribute-name-strategy=none --templates="./components/**/*.html"
The attribute name strategy must match between the server-side build and the client-side attributeMap configuration. If the build uses --attribute-name-strategy=none, import attributeMap from @microsoft/fast-element/attribute-map.js and configure the client with attributeMap({ "attribute-name-strategy": "none" }). Existing declarative imports remain supported.
Writing Components for SSR
When designing components for server-side rendering, keep these guidelines in mind:
- Minimize JavaScript-dependent styling. Prefer CSS-based state (
:hostattributes, CSS custom properties) overelementInternalsstate for initial styles, since the server cannot run JavaScript. - Use
@attrfor initial state. Attributes are visible to the server and can be rendered into the initial HTML. Observable properties set inconnectedCallbackare not available during server rendering. - Keep templates self-contained. Templates should produce meaningful content from attribute values alone without requiring imperative setup.
- Test both paths. Verify that components work correctly with both client-side rendering (no pre-rendered content) and server-side rendering (hydration path).