Navigated to /docs/getting-started/server-and-client-execution

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

TSX
tsximport { Page } from "@tavojs/ui";

export const render = "csr";

export default function BrowserOnlyPage() {
  return <Page>Rendered in the browser</Page>;
}
StageRoute componentUniversal loaderServer-only loader
Initial CSR requestBrowserBrowserSkipped
Initial SSR requestServer, then browser hydrationServer; result is serializedServer; safe result is serialized
Later client navigationBrowserBrowserSkipped
SSG buildBuild processBuild processBuild process
ISR refreshServer runtimeServer runtimeServer 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 pending export 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

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.

Checkpoint

Next steps