Guides

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.

  • ssr

    Stable

    The 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).

    Full explanation

  • ssg

    Stable

    Rendered 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.

    Full explanation

  • csr

    Limited

    The 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.

    Full explanation

  • isr

    Not verified on serverless

    Like 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.

    Full explanation

  • streaming

    Limited

    The 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.

    Full explanation

Who else has it

Show
Render modes: Devora.js compared with full-stack frameworks
FeatureDevora.jsNext.jsReact RouterSvelteKitAstro
Render mode per route
ssrBuilt 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'.
ssgBuilt 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.
csrBuilt 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.
isrBuilt 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.
streamingBuilt 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.