SSR & Streaming SSR
Render on the server per request — including chunked Suspense streams.
On this page
SSR & Streaming SSR
SSR (server-side rendering) runs your components per request on the server, producing HTML before the browser ever sees the page. Prerender does the same thing but once, at build time; SSR is the on-demand version.
When to use SSR
Pick SSR over prerender when:
- The page depends on the current user (session, cookies, JWT).
- Data comes from a fast mutable source (primary DB, feature flags).
- The URL space is too large to enumerate at build time.
- You want fresh data on every request (no stale build).
If none of those apply, prerender is cheaper and faster.
Basic SSR route
Just fetch data in the component. Mandu infers SSR because the data is not available at build time:
// app/account/page.tsx
import { getSession } from "@mandujs/core";
export default async function Account() {
const session = await getSession();
if (!session) return <LoginPrompt />;
const user = await db.user.findOne({ id: session.userId });
return <AccountView user={user} />;
}
No generateStaticParams, no "use client" — Mandu SSRs this route.
Streaming SSR
For pages that wait on slow data, stream the HTML as it resolves using React Suspense:
// app/feed/page.tsx
import { Suspense } from "react";
export default function Feed() {
return (
<main>
<Header /> {/* ships immediately */}
<Suspense fallback={<FeedSkeleton />}>
<Timeline /> {/* slow DB query */}
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<RecommendedUsers /> {/* slow external API */}
</Suspense>
</main>
);
}
Mandu uses renderToStream under the hood. The browser receives the shell
first (Header + skeletons), then each Suspense boundary streams in as its
data resolves. Time-to-first-byte stays near-zero even when total page
takes seconds.
Context inside a server component
The getRequestContext() hook gives you the current request:
import { getRequestContext } from "@mandujs/core";
export default async function Page() {
const ctx = getRequestContext();
const locale = ctx.request.headers.get("Accept-Language") ?? "en";
// ...
}
Combine with the Filling API for handlers with middleware, guards, and lifecycle hooks.
SSR at the edge
Every SSR route compiles for four edge targets without code changes:
mandu build --target=workers # Cloudflare Workers
mandu build --target=deno # Deno Deploy
mandu build --target=vercel-edge # Vercel Edge
mandu build --target=netlify-edge # Netlify Edge
Edge SSR has the same semantics as origin SSR — the only difference is runtime APIs. Node-only packages fail at the edge; see Edge runtimes for the compatibility matrix.
SSR + Islands
SSR and islands compose. The server renders the full tree; islands hydrate the interactive parts:
// app/article/[id]/page.tsx (SSR)
import ReactionsIsland from "./reactions.island";
export default async function Article({ params }: { params: { id: string } }) {
const article = await db.articles.findOne({ id: params.id });
return (
<main>
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.html }} />
<ReactionsIsland articleId={article.id} />
</main>
);
}
Only ReactionsIsland ships JS to the browser. The rest of the article is
static HTML over the wire.
🤖 Agent Prompt
Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/ssr to my project.
Summary of the page:
Mandu SSR renders React components per request via renderToStream. Use for auth'd pages, user-specific content, or data that can't be known at build time. Streaming SSR uses React Suspense to ship HTML as it resolves.
Then:
1. Make the change in my codebase consistent with the page.
2. Run `bun run guard` and `bun run check` to verify nothing
in src/ or app/ breaks Mandu's invariants.
3. Show me the diff and any guard violations.
Related
- Rendering modes — the bigger picture
- Prerender — build-time version of SSR
- Client rendering — when SSR isn't enough
For Agents
Mandu SSR renders React components per request via renderToStream. Use for auth'd pages, user-specific content, or data that can't be known at build time. Streaming SSR uses React Suspense to ship HTML as it resolves.