Server rendering
Render components to HTML on the server and hydrate them in the browser, with the same code on both sides.
Setup
npm install @velozjs/dom @velozjs/ssr
npm install -D vite vite-plugin-velozWith vite-plugin-veloz, your imports of @velozjs/dom resolve to @velozjs/ssr during SSR (in dev with ssrLoadModule and in vite build --ssr), so components don't need to know where they run.
Server entry
import { renderToString } from '@velozjs/ssr';
import { App } from './App';
export function render(url: string) {
return renderToString(() => <App url={url} />); // { html, css }
}HTML shell
<!doctype html>
<html lang="en">
<head>
<!--ssr-css-->
</head>
<body>
<div id="app"><!--ssr-outlet--></div>
<script type="module" src="/src/entry-client.tsx"></script>
</body>
</html>const { html, css } = render(req.url);
res.end(
template
.replace('<!--ssr-css-->', `<style data-veloz-css>${css}</style>`)
.replace('<!--ssr-outlet-->', html)
);Client entry
import { hydrate } from '@velozjs/dom';
import { App } from './App';
hydrate(() => <App url={location.pathname} />, document.getElementById('app')!);Async data
renderToString is synchronous: resources that are still loading render their Suspense fallback. To send the page with its data, use renderToStringAsync:
import { renderToStringAsync } from '@velozjs/ssr';
const { html, css, script } = await renderToStringAsync(() => <App url={req.url} />, {
timeout: 5000,
});
res.end(
template
.replace('<!--ssr-css-->', `<style data-veloz-css>${css}</style>`)
.replace('<!--ssr-outlet-->', html + script) // the script must run before your client entry
);It renders, waits for the pending fetches, and renders again with their values, repeating when one request depends on another. script sets the values for hydrate(), which starts every resource from them instead of fetching.
| Option | Default | |
|---|---|---|
timeout | none | rejects if the data takes longer (ms) |
maxPasses | 10 | limit for chains of dependent requests |
nonce | none | CSP nonce for the data script |
Values must be JSON-serializable; failed loads are sent as their error message. The JSON is escaped so it can't break out of the <script>.
How hydration works
- The server gives each element a
data-hkkey in creation order and wraps every dynamic region in<!--[-->…<!--]-->comments. hydrate()indexes the keys inside its container once, then creates the same tree: elements are reused instead of created, events and effects are attached, and the keys are removed.- Dynamic regions attach to their markers, so static text next to dynamic text is kept as the server sent it.
- Hydration is scoped to the container, so several independent roots on one page work. This site does that: every interactive part is its own small root.
What differs on the server
createEffectandonMountdon't run (they're for browser side effects), so code inside them can usewindowanddocumentfreely. Memos and resources do run. Event handlers aren't printed.innerHTMLis not printed (there is no DOM sanitizer on the server); the client sets it, sanitized, during hydration.Portalrenders nothing; its content is created on the client.renderToStream()returns the same output as a byte stream you can pass tonew Response().
Static sites
The same API prerenders pages at build time: render each route with renderToString (or renderToStringAsync) and write the HTML to disk. This site is built that way, with only its interactive parts hydrated.