Server-Side Rendering#
VoltX can render HTML on the server and hydrate it on the client so that the initial paint is fast and SEO-friendly without sacrificing interactivity.
When SSR Helps#
- Marketing and content-heavy pages that rely on search indexing.
- Dashboards that must show current data immediately on first paint.
- Progressive enhancement flows where the page should work without JavaScript.
- Latency-sensitive experiences served to slow devices or connections.
When CSR Is Enough#
- Highly interactive applications dominated by client-side state.
- Authenticated surfaces hidden from crawlers.
- Rapid prototypes where deployment speed outweighs initial paint.
- Workloads where duplicating rendering logic on the server adds complexity without user benefit.
Rendering Flow#
- Render HTML on the server and embed serialized state.
- Ship that HTML to the browser.
- Call
hydrate()to attach VoltX bindings without re-rendering.
Produce Markup on the Server#
Use serializeScope() to convert reactive state into JSON before embedding it in the HTML you return:
import { serializeScope, signal } from "@volt/volt";
export function renderCounter() {
const scope = {
count: signal(0),
label: "Visitors",
};
const serialized = serializeScope(scope);
return `
<div id="counter" data-volt>
<script type="application/json" id="volt-state-counter">
${serialized}
</script>
<h2 data-volt-text="label">${scope.label}</h2>
<button data-volt-on:click="count++">Clicked <span data-volt-text="count">${scope.count.get()}</span> times</button>
</div>
`;
}
Guidelines:
- Every server-rendered root must have an
id. VoltX looks for<script id="volt-state-{id}">next to it. - The script tag must use
type="application/json"and contain valid JSON. Pretty printing is fine; whitespace is ignored. - Keep serialized data minimal. Fetch large collections after hydration.
Send HTML to the Client#
The HTML you return should already contain the serialized state script tag. VoltX will reuse the DOM structure; do not re-render the same tree on the client.
Hydrate in the Browser#
Call hydrate() once the page loads. It discovers [data-volt] roots automatically.
import { hydrate } from "@volt/volt";
document.addEventListener("DOMContentLoaded", () => {
hydrate({
rootSelector: "[data-volt]",
skipHydrated: true, // defaults to true; repeat calls ignore already hydrated roots
});
});
If you only hydrate a specific block, pass a narrower selector or manually select elements and call hydrate({ rootSelector: "#counter" }).
Serialized State#
VoltX exposes helpers that mirror the runtime's internal behavior:
| Helper | Action |
|---|---|
serializeScope(scope) |
Converts signals into their raw values before you embed them |
deserializeScope(data) |
Restores a JSON payload into a fresh scope. Useful for streaming responses or server actions that return HTML partials |
isServerRendered(element) |
Tells you if VoltX found a matching serialized state block |
isHydrated(element) |
Detects whether a root has already been hydrated, which is handy when mixing SSR content with dynamic client mounts |
getSerializedState(element) |
Reads the JSON payload for debugging or custom hydration flows. |
import { deserializeScope, getSerializedState } from "@volt/volt";
const root = document.querySelector("#counter")!;
const state = getSerializedState(root);
if (state) {
const scope = deserializeScope(state);
console.log(scope.count.get()); // -> 0
}
Avoiding Flash of Unstyled Content#
Hydrated markup should look identical before and after VoltX runs. When CSS or font loading causes flicker, consider these patterns:
Hide Until Hydrated#
<style>
[data-volt]:not([data-volt-hydrated]) {
visibility: hidden;
}
[data-volt][data-volt-hydrated] {
visibility: visible;
}
</style>
VoltX sets data-volt-hydrated="true" once hydrate() completes, so you can safely reveal content at that point.
Use a Loading Overlay#
<div id="app" data-volt>
<!-- server-rendered content -->
</div>
<div class="loading-overlay">Loading…</div>
<script type="module">
import { hydrate } from "@volt/volt";
hydrate();
</script>
.loading-overlay {
position: fixed;
inset: 0;
background: white;
display: grid;
place-items: center;
}
[data-volt-hydrated] ~ .loading-overlay {
display: none;
}
Progressive Enhancement#
Render functional HTML that works without JavaScript, then let VoltX enhance it:
<form id="contact" method="POST" action="/submit" data-volt>
<script type="application/json" id="volt-state-contact">
{ "submitted": false }
</script>
<input type="email" name="email" required />
<p data-volt-if="submitted" data-volt-text="'Thank you!'"></p>
<button type="submit">Submit</button>
</form>
Security Checklist#
- Escape user-generated content in the HTML you render.
- Validate JSON before embedding it in
<script type="application/json">tags. - Apply a strict Content Security Policy (CSP) so inline scripts are controlled.
- Never serialize secrets. Treat the hydrated payload as public data.
Pair these guidelines with the lifecycle hooks documented in lifecycle to coordinate mount-time work across SSR and client renders.