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.