Render modes
Every route declares how it renders, explicitly, with a renderMode export. A route that exports nothing inherits its app's defaultRenderMode from app.config.ts (which itself defaults to ssr). Nothing is chosen for you based on file location or data-fetching patterns.
Choose how each route renders
Every route says how it renders, in its own file. Choosing a mode per route is not unique to Devora.js — the table below shows who else does it. The narrow claim here is only that the choice is one explicit line in the route file.
StablessrThe loader runs on every request and the page is rendered fresh.
Use it for: Per-user or per-request data, anything behind a login.
export const renderMode = "ssr";Limits
The default for a route that exports nothing (unless the app sets another default).
StablessgRendered once during devora build and served as static HTML.
Use it for: Content that does not change per request: docs, marketing, blog posts.
export const renderMode = "ssg";Limits
A dynamic route needs getStaticParams(). In dev it renders live per request; it is pre-rendered only by a production build.
LimitedcsrThe server sends an empty shell; the component mounts in the browser.
Use it for: Pages that only make sense in a browser, behind your own client data layer.
export const renderMode = "csr";Limits
The loader never runs, and the page needs the client bundle to show anything. Import from @devorajs/core/client.
Not verified on serverlessisrLike ssg, but regenerated once a revalidate window has passed.
Use it for: Mostly static content that should refresh on a schedule.
export const renderMode = "isr"; export const revalidate = { seconds: 3600 };Limits
Reliable under a long-lived Node server. On Vercel/Netlify the function's filesystem is not guaranteed to persist, so ongoing regeneration there has not been verified.
LimitedstreamingThe shell is sent at once; island content streams in as it resolves.
Use it for: A page with one slow section you do not want the whole response to wait for.
export const renderMode = "streaming";Limits
GET only (no action). Delivered as one buffered response on Netlify; not verified on a live deployment elsewhere.
Who else has it
| Feature | Next.js | React Router | SvelteKit | Astro | |
|---|---|---|---|---|---|
| Render mode per route | |||||
ssr | Built inrenderMode = "ssr" in the route file. | Built inDynamic rendering of a route. | Built inssr: true; an app-wide setting. | Built inThe default; export const ssr per page. | Built inprerender = false per page, or output: 'server'. |
ssg | Built inrenderMode = "ssg" and getStaticParams(). | Built inPrerendered; generateStaticParams for dynamic routes. | Built inprerender() in config lists the URLs. | Built inexport const prerender = true per page or layout. | Built inThe default; prerender = true per page. |
csr | Built inrenderMode = "csr"; the loader never runs. | PartialClient Components, or a single-page-app setup; not a per-route mode. | Built inssr: false (SPA mode, app-wide); clientLoader per route. | Built inexport const ssr = false per page or layout. | Not verifiedClient islands exist; a whole-page client mode was not checked. |
isr | Built inrevalidate = { seconds }; not verified on serverless. | Built inexport const revalidate = 60; per-instance cache unless you add a shared handler. | Not built inNot described on the pages checked. | PartialOnly through adapter-vercel's isr option. | PartialOnly through the Vercel adapter's isr option. |
streaming | Built inrenderMode = "streaming"; GET only; buffered on Netlify. | Built inSuspense and loading.js. | Built inReturn promises from loaders; Await in Suspense. | Built inPromises from a server load; buffered where the platform cannot stream. | Built inHTML streaming in on-demand rendering. |
Checked against each framework's own documentation on October 16, 2026; a cell says "Not verified" where that page was not checked. Sources: Next.js: Incremental Static Regeneration · Next.js: Streaming · React Router: Rendering strategies · React Router: Streaming with Suspense · SvelteKit: Page options · SvelteKit: Loading data (streaming) · SvelteKit: adapter-vercel (ISR) · Astro: On-demand rendering · Astro: Vercel adapter (ISR). Next.js's ssr, ssg and csr cells are read from the two Next.js pages above and the Next.js docs they link to.
ssr Server-rendered per request
The default. The route's loader runs on every request, and the page is rendered fresh each time.
export const renderMode = "ssr";
export async function loader() {
return { renderedAt: new Date().toISOString() };
}
export default function Page({ data }) {
return <p>Rendered at: {data.renderedAt}</p>;
}ssg Pre-rendered at build time
The route is rendered once during devora build and served as static HTML from then on. Good for content that doesn't change per request — this docs site itself uses ssg for every page, since there's no login and content only needs to render once to be cached.
export const renderMode = "ssg";
export async function loader() {
return {};
}
export default function AboutPage() {
return <p>This page was rendered once, at build time.</p>;
}In dev, ssg (and isr) routes still render live per request, for a faster edit loop — the build-time pre-render only applies to devora build output.
csr Client-only
The server sends a minimal shell with no content; the route's actual component only mounts once a small client bootstrap script imports and renders it in the browser. No loader ever runs for a csr route.
import { PageShell } from "@devorajs/core/client";
// note the /client subpath — a csr route is bundled for the browser too,
// and the main @devorajs/core entry pulls in server-only code (node:crypto,
// node:fs) that can't bundle for a browser target.
export const renderMode = "csr";
export default function CsrDemo() {
return <p>Mounted client-side at: {new Date().toISOString()}</p>;
}isr Static with scheduled revalidation
Like ssg, but the pre-rendered page is regenerated once a revalidate window passes (or on a manual revalidatePath() call). The revalidation trigger is explicit and declared in the route file, not a hidden multi-layer cache.
export const renderMode = "isr";
export const revalidate = { seconds: 3600 };
export async function loader() {
return { renderedAt: new Date().toISOString() };
}
export default function IsrPage({ data }) {
return <p>Rendered at: {data.renderedAt}</p>;
}Ongoing revalidation is fully reliable under a long-lived Node process (self-hosted or Docker). On a serverless platform (Vercel/Netlify), a function's filesystem isn't guaranteed to persist or be shared across invocations, so the initial build's static content still serves correctly but ongoing regeneration there is comparatively less battle-tested — worth confirming for your own workload if you lean heavily on isr in a serverless deployment.
streaming Chunked SSR
Available since @devorajs/core@0.2.2. The page shell sends immediately; an island's real content patches in once its import resolves, instead of the whole response waiting on it. Needed a real, separate rendering strategy under the hood — the two-pass model every other render mode uses (render once, collect any unresolved islands, re-render) doesn't apply to a response that's already streaming to the browser; renderMode: "streaming" uses React's own Suspense contract instead, so renderToPipeableStream can send a real fallback immediately and swap in the real content later, natively.
import { island, Island } from "@devorajs/core";
const SlowWidget = island(() => import("../components/SlowWidget"));
export const renderMode = "streaming";
export async function loader() {
return { renderedAt: new Date().toISOString() };
}
export default function StreamingPage({ data }) {
return (
<>
<p>Rendered at: {data.renderedAt}</p>
<Island component={SlowWidget} props={{}} />
</>
);
}Nothing changes about how <Island> is used — the same component works under ssr or streaming with no code changes; which strategy it uses is decided by which render mode is actively rendering it. GET-only, on purpose: an action needs to decide "redirect or re-render" before any HTML is sent, which conflicts with a response that's already streaming — the same restriction ssg/isr already have, for an unrelated reason. Under devora start the response really streams. The generated Vercel function uses the same code path, which has not been verified on a live deployment; Netlify's function format delivers the same HTML as one buffered response.
Islands and client-only components
Independent of render mode, an individual component can opt into island hydration — island(() => import("./ThreeScene")) — so only that component hydrates on the client while the rest of an SSR/SSG/ISR page stays static HTML. A component that must never run on the server at all (touches window at module scope, say) uses clientOnly(() => import("./Widget")) instead, which renders nothing server-side and only resolves in a real browser.
Islands are found while the build follows your app's imports, so an island() can live in a shared component or a workspace package, not only in a route file. The call must have the literal shape island(() => import("./path")) so the build knows which file to ship to the browser; anything else (island(loader), a computed path, a path that doesn't resolve) stops devora dev and devora build with the file and line, instead of shipping an island that never hydrates.