Server and client execution
Choose where route work runs, protect private dependencies, and keep the first browser render consistent with server HTML.
Understand the rendering model
Tavo.js renders the same TSX component model on the server and in the browser. In an SSR runtime, routes render on the server by default and hydrate in the browser. A route can opt into client-only rendering when its initial HTML is not important.
Create src/pages/browser-only.tsx
tsximport { Page } from "@tavojs/ui";
export const render = "csr";
export default function BrowserOnlyPage() {
return <Page>Rendered in the browser</Page>;
}| Stage | Route component | Universal loader | Server-only loader |
|---|---|---|---|
| Initial CSR request | Browser | Browser | Skipped |
| Initial SSR request | Server, then browser hydration | Server; result is serialized | Server; safe result is serialized |
| Later client navigation | Browser | Browser | Skipped |
| SSG build | Build process | Build process | Build process |
| ISR refresh | Server runtime | Server runtime | Server runtime |
SSR renders for the current request.
CSR sends the document shell and resolves the route in the browser.
SSG prerenders selected static routes during the build.
ISR serves cached SSR output and refreshes it after a revalidation interval.
A route
pendingexport can appear during browser route resolution, but normal SSR and static generation wait for the completed page or its error view.
Keep private work behind a server boundary
defineServerLoader marks route data that must only resolve on the server. Tavo.js also enforces src/server/** as a server-only module boundary: the client build fails if code from that directory reaches its module graph. Put databases, sessions, private API clients, and secret-bearing code there, then reach it through server loaders, actions, middleware, or server routes.
A server-only loader is skipped during browser route resolution. Its SSR result is available during initial hydration, but a later client navigation needs safe client auth state or a server endpoint such as /api/me when it must refresh that data.
Create src/pages/account.tsx
tsximport { defineServerLoader } from "@tavojs/core/router";
import { Page, Text } from "@tavojs/ui";
type Account = { name: string };
export const load = defineServerLoader(async ({ request, signal }) => {
const { getAccount, readSession } = await import("./session.server");
const session = await readSession(request);
if (!session) throw new Error("Authentication required");
return getAccount(session.accountId, { signal });
});
export default function AccountPage({ data }: { data?: Account }) {
return (
<Page>
<Text>Welcome, {data?.name}</Text>
</Page>
);
}
Keep hydration deterministic
The browser hydrates against the route data and HTML resolved by the server. A hydration warning means the initial client tree did not reproduce that output.
Guard browser-only globals such as window and
localStorage.Avoid time, randomness, and locale differences in the initial render.
Use deterministic IDs from framework helpers.
Test the production SSR build, not only the development server.