Rendering Model
Choose dynamic rendering, static rendering, ISR, or PPR with route-level exports and config.
Rendering controls decide when HTML is produced. Route Runtime separately decides where a dynamic page or API handler executes and how deployment limits are applied.
Dynamic rendering, static rendering, and revalidation apply to every renderer. The component examples below use React-compatible TSX; see Renderers for Preact, Solid, Vue, and Svelte conventions and the features that remain React-specific.
Rendering options
RSC page URLs return HTML for document visits and a Flight payload for requests accepting
text/x-component. Both representations include Vary: Accept. When you enable shared caching,
configure the CDN or reverse proxy to honor Vary; do not cache these responses by URL alone.
Farm preserves existing Vary fields, including Origin and Nitro's Accept-Encoding.
| Mode | How to opt in | Best for |
|---|---|---|
| Dynamic | Default for request-bound pages | Dashboards and personalized UI. |
| Static | dynamic = force-static or use static directive | Marketing pages and stable docs. |
| ISR | revalidate = seconds | Content that can refresh on a schedule. |
| PPR | experimental_ppr = true | Static shells with dynamic holes. |
Route-level config
export const dynamic = "force-static";
export const revalidate = 300;
export default async function PricingPage() {
return <main>Pricing</main>;
}Use directives when compactness wins
Farm also recognizes compact rendering directives at the top of route modules. This keeps small examples readable while preserving explicit exports for Next-style compatibility.
"use ssg; 60";
export default function BlogPage() {
return <main>Blog</main>;
}use ssg; 60 statically generates the route and revalidates it every 60 seconds. Farm also accepts
use ssg, use dynamic, and use ppr; 60 when those rendering modes fit the route.
Dynamic rendering
Use dynamic rendering for request-specific pages such as dashboards, account settings, and pages that depend on cookies, headers, or per-user data.
With experimental React Server Components enabled, Farm streams HTML while inserting the styles, hydration payload, and client script. HTML injection preserves UTF-8 bytes across stream chunks, including emoji and non-ASCII content, without buffering the entire page.
export const dynamic = "force-dynamic";
export default async function DashboardPage() {
return <main>Dashboard</main>;
}Static rendering
Use static rendering for pages that can be built once and served quickly.
During a production build, Farm also scans routes that use the default dynamic mode. If a non-parameterized route does not read cookies, headers, authentication, search parameters, or middleware request data, Farm reports it as a static-rendering candidate. The build keeps the route dynamic until you opt in, because request-bound work may be hidden in an imported component or data loader.
Review each suggestion, including its imported code, then add the static export when it is safe. This keeps personalized pages dynamic while making stable pages easy to move onto the fastest rendering and caching path.
If an explicitly static route directly reads request APIs or request props, Farm reports the unsafe read and keeps the route dynamic instead of freezing request-specific HTML into a shared artifact. This check covers the route module itself; continue reviewing imported components and data loaders, because request-bound work in transitive imports cannot be proven safe from the route source alone.
Farm serves generated HTML only for GET and HEAD requests. Other request methods continue through
the live route instead of receiving a cached page document.
export const dynamic = "force-static";
export default function AboutPage() {
return <main>About Farm</main>;
}ISR-style revalidation
revalidate caches a static response and refreshes it after the configured number of seconds.
export const dynamic = "force-static";
export const revalidate = 300;
export default async function PricingPage() {
const plans = await loadPlans();
return <PricingTable plans={plans} />;
}PPR with Suspense
PPR is for pages with a stable shell and dynamic sections. Put dynamic work behind Suspense boundaries so the shell can be cached while the slower section resolves independently.
import { Suspense } from "react";
export const experimental_ppr = true;
export const revalidate = 60;
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
<Suspense fallback={<RevenueSkeleton />}>
<RevenuePanel />
</Suspense>
</main>
);
}Deferred route islands
The route-level hydration strategy is shared by renderer adapters. Preact uses the same nested
"use client" analysis through its compatibility runtime; Solid, Vue, and Svelte currently hydrate
at the route boundary selected by their adapter.
Client routes and server routes that import a client boundary can defer their JavaScript while Farm
keeps the server-rendered HTML visible. Add an optional static island export to the client module:
"use client";
export const island = "interaction";
export function CopyButton({ value }: { value: string }) {
return <button onClick={() => navigator.clipboard.writeText(value)}>Copy</button>;
}Farm propagates the strategy to the route hydration boundary, emits it in the route manifest, and splits production route modules behind dynamic imports. Deferred routes therefore avoid evaluating their route chunk until its trigger while preserving the initial SSR output.
These strategies control hydration of the initial server-rendered document. During client-side navigation, the navigation itself signals user intent, so Farm loads and renders a route-wide destination immediately instead of leaving the previous route visible while waiting for another trigger.
| Strategy | Hydration trigger |
|---|---|
load | Immediately. This is the default and compatibility-first behavior. |
interaction | The first button-like click; Farm replays that click after hydration. |
visible | When the route boundary approaches the viewport. |
idle | During browser idle time, with a timeout fallback. |
With isolated client hydration, each eligible client module keeps its own strategy, including after SPA navigation. Sibling boundaries can mix all four strategies: one boundary's trigger never hydrates another, and an interaction click is claimed and replayed once by the boundary that contains it. Removing a boundary cancels its pending observer, idle callback, or interaction listener.
Farm uses the same isolated boundary metadata for Vite development, streamed or buffered SSR, and statically generated HTML. In development, updating a client module rerenders every live boundary created from that module without importing or replacing its server-owned layout; sibling boundary state stays mounted. If one page needs the route-wide fallback, that fallback stays on the page boundary and does not promote otherwise eligible client leaves in its server-owned layout. Farm also keeps graphs above four statically bounded isolated roots route-wide. Data-dependent boundary lists use the same fallback because their root count is unknown before streaming. This measured guard prevents request, marker, root, and hydration overhead from growing past the first observed crossover; the benchmark report includes route-wide, isolated, and RSC controls with raw samples. The representative fixtures also run route-wide and isolated hydration with Farm's experimental React compiler enabled, proving both the initial hydration cost and repeated state-update cost of the combined path.
The export must be one of these static string literals so Farm can analyze it without executing
application code. Without an explicit route-level island export, a route that imports client
boundaries with different strategies safely falls back to load because its current route-level
React-compatible root cannot schedule those children independently. Keep interactive leaves small today; a
eligible leaves can also use Farm's experimental React compiler for direct state binding updates
after their independent roots mount.
Async pages stay server-only
React and Preact cannot hydrate an async component in the browser. When a page's default export is async
and it imports client components (or exports hydrate = true), Farm keeps the route
server-rendered instead of hydrating it: the SSR HTML stays visible, but the imported client
components are not interactive on that route. Farm logs a warning pointing at the module when this
happens. To make the interactivity work, fetch data in a synchronous page (for example through a
route loader) and render the "use client" component from there, or enable experimental server
components support.
Isolated client leaves without RSC
"use client" also remains available in a normal React SSR application. Set
experimental.isolatedClientHydration to "analyze" to audit which local client leaves Farm can
split without changing runtime behavior, or to "enabled" to hydrate safe leaves independently.
Unsupported routes keep the existing route-wide ownership model. See
Isolated client hydration for the modes and safety
rules. This experiment is disabled when RSC owns the route. Integration providers also keep the
route-wide root unless they explicitly declare that they can be recreated around independent
isolated roots.
Automatic optimized boundaries
Automatic optimized boundaries are a React-only experiment and are not applied to Preact, Solid, Vue, or Svelte routes.
Farm can experimentally render large, non-interactive Server Component regions through the native
Strata renderer. Enable the flag once in farm.config.ts:
import { defineConfig } from "@farm.js/core";
export default defineConfig({
experimental: {
serverComponents: true,
optimizedBoundary: true,
},
});Application components remain ordinary JSX:
export default function ArticlePage() {
return (
<article className="prose">
<h1>Representation-aware rendering</h1>
<p>Farm selects this host-only region automatically.</p>
</article>
);
}Farm evaluates eligible host-element trees on the server and uses Strata only when the region is large enough to benefit. Trees containing Client Components, event handlers, refs, unsupported elements or attributes, unsafe URLs, or other uncertain values retain normal React rendering. The flag does not require an application boundary component or a direct Strata dependency.
The current Strata runtime uses a native Node binding. Keep this experiment on Node deployments until a Wasm or JavaScript fallback is available for edge and Cloudflare worker targets.
Choosing a mode
| Need | Use |
|---|---|
| User-specific data on every request | dynamic = "force-dynamic" |
| Stable docs, marketing, or policy page | dynamic = "force-static" |
| Stable page with scheduled refresh | revalidate = 60 |
| Static shell plus dynamic holes | experimental_ppr = true with Suspense |