Component Architecture
Master the React 19 dual-component model, serializable flight boundaries, component-level micro-caching with <DinouCacheSlot>, Partial Prerendering (PPR), and high-performance rendering strategies in Dinou.
1. Overview & Dual-Component Model
Dinou is built natively on the React 19 Server Components (RSC) specification. Rather than treating your entire application as a single monolithic client-side bundle, Dinou partitions components into two distinct execution environments:
"use client".useState), event listeners (onClick), and browser APIs.1.1. The RSC Mental Model
Think of Server Components as the backbone of your application that orchestrates data fetching, layouts, and non-interactive UI (rendering pure HTML with zero client JavaScript). Client Components act as interactive "islands" or leaves embedded inside the server tree.
The server streams HTML and React Flight JSON representations across an open HTTP stream. The browser consumes this stream without ever downloading the JavaScript source code of your Server Components.
2. Server Components (RSC)
In Dinou, every component is a Server Component by default unless explicitly marked with "use client". They execute exclusively on the server and never ship their implementation code to the browser.
2.1. Capabilities & Backend Access
- Direct Async Execution: Server Components can be
asyncfunctions. You canawaitdatabase queries, microservices, or files directly inside the component body. - Zero Client Bundle Impact: Large dependencies imported into Server Components (e.g., Markdown parsers, SQL drivers, encryption packages) are never downloaded or executed by the browser.
- Secure Secret Access: Database credentials, private API keys, and environment variables stay strictly on the server without risk of client exposure.
- Automatic Streaming with <Suspense>: When slow asynchronous operations are wrapped in React
<Suspense>, the layout shell and fallback UI can be delivered immediately without waiting for slow queries, while data streams progressively into the HTTP response.
// src/users/page.tsx (Server Component by default)
import { getContext } from "dinou";
import { db } from "@/lib/db"; // Never bundled to the client!
export default async function UsersPage() {
// Direct backend query without an intermediary REST API
const users = await db.user.findMany({ take: 10 });
return (
<main className="p-8 max-w-4xl mx-auto">
<h1 className="text-2xl font-bold">Registered Users</h1>
<ul className="mt-4 divide-y">
{users.map((user) => (
<li key={user.id} className="py-2 flex justify-between">
<span>{user.name}</span>
<span className="text-xs font-mono text-muted-foreground">{user.email}</span>
</li>
))}
</ul>
</main>
);
}TTFB & Execution Timing Note
Static by Default (SSG): In Dinou, this page is pre-rendered at build time by default. The database query runs during npm run build, serving static HTML to visitors with instant TTFB (0ms server wait).
Dynamic Routes: If the route is marked dynamic (e.g. revalidate = 0 in page_functions.ts or reading cookies), a top-level await blocks the entire HTTP response until the query finishes. To preserve instant TTFB on dynamic routes, delegate the await to a child component wrapped in React <Suspense>.
Non-Blocking Streaming with React <Suspense>
By delegating asynchronous queries to child components wrapped in <Suspense>, the outer layout and fallback UI are sent immediately:
// src/users/page.tsx - Non-blocking streaming layout
import { Suspense } from "react";
import { db } from "@/lib/db";
import { UsersSkeleton } from "./skeleton";
export default function UsersPage() {
return (
<main className="p-8 max-w-4xl mx-auto">
{/* 1. The outer shell is sent immediately (instant TTFB) */}
<h1 className="text-2xl font-bold">Registered Users</h1>
{/* 2. Displays fallback while the child query resolves */}
<Suspense fallback={<UsersSkeleton />}>
<UsersList />
</Suspense>
</main>
);
}
// Child component handles the async query independently
async function UsersList() {
const users = await db.user.findMany({ take: 10 });
return (
<ul className="mt-4 divide-y">
{users.map((u) => (
<li key={u.id} className="py-2 flex justify-between">
<span>{u.name}</span>
<span className="text-xs font-mono text-muted-foreground">{u.email}</span>
</li>
))}
</ul>
);
}How <Suspense> Behaves Across Rendering Strategies
The behavior of <Suspense> and fallback skeletons changes fundamentally depending on whether your route is static, dynamic, or hybrid:
Build-Time Resolution: During npm run build, the compiler awaits the <Suspense> promise to resolve completely and writes the final resolved HTML directly into the static file.
Fallback Behavior: Visitors never see the loading skeleton because the fallback is eliminated once the build finishes.
Trade-off: 0ms TTFB from CDN edge, but data reflects build-time state (stale until next build/revalidation).
On-Demand Streaming: On every HTTP request, the server executes the route and immediately streams the shell and <Suspense fallback={<Skeleton />}> skeleton over the open HTTP connection.
Fallback Behavior: The user sees the skeleton while waiting, and React streams the resolved HTML chunk when the query finishes.
Trade-off: 100% fresh data and non-blocking layout, but incurs continuous server compute costs, DB load, and request-time latency.
Static Shell Prerender: The outer layout shell and the fallback skeleton are pre-rendered at build time and served immediately with 0ms TTFB from the CDN edge.
Fallback Behavior: The skeleton renders instantly from static cache, while the server concurrently executes only the dynamic query and streams live data into the hole.
Trade-off: Combines the instant delivery of SSG with the fresh, live data of dynamic SSR at minimal server compute cost.
2.2. Constraints & Limitations
Because Server Components do not execute in the browser, they cannot use browser-specific or client-interactive features:
- Cannot use state or lifecycle hooks (
useState,useEffect,useReducer). - Cannot add interactive client DOM event listeners (e.g.
onClick,onChange,onSubmit). - Cannot access browser-only globals (
window,document,localStorage,navigator). - Cannot consume (read) React Context with
useContext()oruse(Context). (While Server Components can render<Context>providers for client children, they cannot read context values themselves).
3. Client Components
Client Components provide rich, real-time client-side interactivity. They enable event handlers, local component state, animations, and access to browser APIs.
3.1. Interactivity & Browser APIs
Client Components have complete access to the client-side React ecosystem:
- Full state management with
useState,useReducer, and React 19useActionState. - Side effects, timers, and DOM subscriptions via
useEffectanduseLayoutEffect. - DOM event handling (clicks, hovers, drags, keystrokes, form submissions).
- Browser Web APIs (
localStorage,Geolocation,IntersectionObserver,Audio). - Consuming React Context providers created on the client.
3.2. Server Pre-Rendering Notice
A common point of confusion is assuming Client Components execute exclusively in the browser. Client Components are pre-rendered on the server to HTML during initial page load to ensure fast First Contentful Paint (FCP) and SEO indexing.
SSR Execution Safety Rule
const token = localStorage.getItem("token")) will throw a server runtime error. Defer browser-only access inside a useEffect hook or guard it with if (typeof window !== "undefined").4. The "use client" Directive
To declare a component as a Client Component, add the "use client" directive at the very top of the file, before any import statements or comments.
4.1. Establishing Module Boundaries
The directive is not a runtime function call—it is an instruction to the bundler (esbuild, Rollup, Webpack) that defines the network split point:
"use client"; // Must be at the very top of the file!
import { useState } from "react";
export function Counter({ initial = 0 }: { initial?: number }) {
const [count, setCount] = useState(initial);
return (
<button
onClick={() => setCount((c) => c + 1)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium"
>
Count: {count}
</button>
);
}4.2. Import Cascading Behavior
Once a file declares "use client", all components, utility modules, and third-party libraries imported by that file are automatically treated as part of the client bundle. You do not need to add "use client" to child modules imported by a Client Component.
5. Data Serialization Boundary
When passing props from a Server Component to a Client Component, data crosses the network boundary (serialized as React Flight JSON). Props passed across this boundary must be serializable.
5.1. Supported Data Types
- Primitives:
string,number,boolean,null,undefined. - Plain Objects & Arrays: Objects with serializable values (e.g.
{ id: 1, name: "Dinou" }). - Dates:
Dateobjects are serialized and reconstructed by React. - React Elements (JSX): Server Components passed as
childrenor named slots. - Promises: Passed directly from Server to Client (read with React 19
use()). - Server Functions: Bound actions declared with
"use server".
- Client Functions: Cannot pass client callbacks (e.g.
onClick={() => ...}) from Server Components. - Class Instances: Complex prototypes, database connections, and class instances (e.g. Prisma models with methods) will lose their prototype.
- Symbols & WeakMaps: Non-enumerable or memory-bound JavaScript data structures.
- Browser DOM Elements: Native HTML DOM nodes (
window,document.body).
5.2. Passing Promises & React 19 use()
React 19 supports passing an unresolved Promise as a prop from a Server Component to a Client Component. The Client Component reads the promise synchronously using the use() hook, automatically triggering Suspense fallbacks until the promise settles:
// Server Component
import { Suspense } from "react";
import { UserProfile } from "./user-profile"; // "use client"
export default function Page() {
// Start fetching without awaiting!
const userPromise = fetchUserData();
return (
<Suspense fallback={<div>Loading profile skeleton...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// Client Component (user-profile.tsx)
"use client";
import { use } from "react";
export function UserProfile({ userPromise }: { userPromise: Promise<{ name: string }> }) {
// Unwraps the promise value seamlessly
const user = use(userPromise);
return <h2>Hello, {user.name}</h2>;
}6. Architectural Patterns & Best Practices
Dinou empowers you to choose the exact architectural balance between server performance and client ergonomics. Follow these battle-tested patterns for optimal architecture:
6.1. Interactive Leaves at the Edges (Recommended)
Keep your layouts, route pages, and static wrappers as Server Components. Only push the "use client" boundary down to the small, interactive "leaf" components (such as an upvote button, search box, or dropdown menu).
6.2. Composition: Server Components as Children
A Client Component cannot directly import a Server Component (because bundling a Server Component into the client module would defeat the RSC boundary). However, a Client Component can receive Server Components as children or via named props!
// Client Component: ModalContainer.tsx
"use client";
import { useState, type ReactNode } from "react";
export function ModalContainer({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
{isOpen && (
<div className="modal-backdrop">
{/* children is rendered on the server and passed as pre-rendered JSX! */}
{children}
</div>
)}
</div>
);
}
// Server Component: page.tsx
import { ModalContainer } from "./ModalContainer";
import { HeavyServerProfile } from "./HeavyServerProfile"; // Server Component
export default function Page() {
return (
<ModalContainer>
{/* HeavyServerProfile remains 100% on the server! */}
<HeavyServerProfile />
</ModalContainer>
);
}6.3. Client-First with Server Data (getProps)
If you are building an interactive canvas, video editor, or complex SPA dashboard and prefer traditional React where you can use useState and onClick anywhere in the page without boundary splitting, use Dinou's Client-First Model:
// src/canvas/page_functions.ts - Runs securely on the server
export async function getProps(params: { id: string }) {
const project = await db.projects.findById(params.id);
return { project };
}
// src/canvas/page.tsx - Entire page is a Client Component
"use client";
import { useState } from "react";
export default function CanvasPage({ project }: { project: any }) {
// Full access to state, effects, and events everywhere in the file!
const [layers, setLayers] = useState(project.initialLayers);
return (
<div onClick={() => console.log("clicked")}>
<Toolbar onAddLayer={(l) => setLayers([...layers, l])} />
<Stage layers={layers} />
</div>
);
}Advantage: Fast development speed, traditional React ergonomics, with secure backend data fetching and SSR pre-rendering intact.
6.4. Granular Caching with <DinouCacheSlot> (Vertical Micro-ISR)
A common architectural trap in dynamic RSC applications is converting heavy server widgets into Client Components just to gain client-side memory caching (such as SWR or TanStack Query). In Dinou, <DinouCacheSlot> provides component-level server caching (Vertical Segmentation) directly in the RSC layer.
To avoid running an expensive 2-second database query on every dynamic page load, developers often convert the widget to "use client" with a client cache. This bloats the client JS bundle, loses direct database access, exposes internal API endpoints, and introduces client-side network waterfalls.
Keep the component on the server! <DinouCacheSlot> caches the rendered RSC Flight payload of that specific subtree on the server for a designated TTL (e.g. 300s). The rest of the page remains 100% dynamic, while the expensive subtree serves in 0ms.
Can a <DinouCacheSlot> Contain Client Components?
Yes, absolutely. While <DinouCacheSlot> itself is an async Server Component that executes exclusively on the server, its children can freely render interactive Client Components. Dinou caches the Client Component reference along with its pre-computed props. The client hydrates it normally with full interactivity (events, state, animations):
// 1. Client Component: interactive-chart.tsx (Hydrates with 100% interactivity)
"use client";
import { useState } from "react";
export function InteractiveChart({ data }: { data: any }) {
const [zoom, setZoom] = useState(1);
return (
<div className="p-4 border rounded-lg bg-card">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-bold">Metrics (Zoom: {zoom}x)</span>
<button
onClick={() => setZoom((z) => z + 1)}
className="px-2 py-1 text-xs bg-primary text-primary-foreground rounded"
>
Zoom In
</button>
</div>
<canvas data-zoom={zoom} />
</div>
);
}
// 2. Server Component: page.tsx (Dynamic page with an isolated cached slot)
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou";
import { db } from "@/lib/db";
import { UserGreeting } from "./UserGreeting"; // Reads user session/cookies
import { GreetingSkeleton, AnalyticsSkeleton } from "./skeletons";
import { InteractiveChart } from "./interactive-chart"; // Client Component
// Server Component performing the heavy query
async function HeavyAnalyticsWidget() {
const analyticsData = await db.analytics.aggregateBigData(); // 2-second query
return <InteractiveChart data={analyticsData} />;
}
export default function DashboardPage() {
return (
<main className="space-y-6 p-8">
{/* Dynamic personal component: reads cookies/session without blocking initial layout delivery (preserves fast TTFB) */}
<Suspense fallback={<GreetingSkeleton />}>
<UserGreeting />
</Suspense>
{/* Micro-ISR hole: wrapped in Suspense so cache misses stream without blocking TTFB */}
<Suspense fallback={<AnalyticsSkeleton />}>
<DinouCacheSlot id="analytics-slot" duration={300} tags={["analytics"]}>
<HeavyAnalyticsWidget />
</DinouCacheSlot>
</Suspense>
</main>
);
}Why wrap in <Suspense>? While <DinouCacheSlot> works without it on cache hits, on a cache miss (cold start or expired TTL) an unwrapped slot would block the entire HTTP response during the heavy query. Wrapping it in <Suspense> guarantees instant TTFB by streaming the layout and skeleton immediately.
What about UserGreeting? In dynamic SSR, wrapping UserGreeting in <Suspense> ensures that reading user session or auth cookies does not delay the initial response, keeping TTFB fast. In PPR, it also prevents dynamic request data from de-optimizing the build-time static shell.
In-Place Client Refresh: Any Client Component can trigger an on-demand re-render of only this slot using refreshSlot("analytics-slot") from "dinou" without reloading the page or resetting state in other components.
On-Demand Invalidation: Server Functions and webhooks can invalidate the slot cache immediately via revalidateTag("analytics").
6.5. The Synergy: Partial Prerendering (PPR) + <DinouCacheSlot>
What happens when you nest <DinouCacheSlot> inside a React <Suspense> boundary on a page with Partial Prerendering (PPR)? You unlock the highest performance profile possible in full-stack web engineering: zero-delay initial delivery paired with near-zero streaming delay and 99.9% database query offloading.
• TTFB: 0ms from CDN edge (instant outer layout).
• Skeleton Duration: The user waits the full query duration (e.g. 1,200ms) on every single request while the server executes the query.
• Database Load: 10,000 visitors per minute = 10,000 database queries executed on the server.
• TTFB: 0ms from CDN edge (instant outer layout).
• Skeleton Duration: ~5ms! Subsequent visitors receive the cached RSC stream chunk almost immediately; the skeleton barely blinks.
• Database Load: 10,000 visitors per minute with duration={60} = 1 database query per minute (99.99% reduction).
Implementation Example
Wrap the slow asynchronous Server Component with <DinouCacheSlot>, and then enclose both inside React <Suspense>:
// src/dashboard/page.tsx - High-traffic operational dashboard
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou";
import { db } from "@/lib/db";
import { MetricsSkeleton } from "./MetricsSkeleton";
import { InteractiveMetricsChart } from "./InteractiveMetricsChart"; // "use client"
// Heavy async Server Component
async function HeavyMetricsSubtree() {
// Heavy aggregation across millions of database rows (e.g. 1.5 seconds)
const aggregatedStats = await db.telemetry.aggregateOperations();
return <InteractiveMetricsChart data={aggregatedStats} />;
}
export default function DashboardPage() {
return (
<main className="p-8 space-y-6 max-w-5xl mx-auto">
{/* 1. PPR Static Shell: Delivered instantly with 0ms TTFB from edge CDN */}
<header className="border-b pb-4">
<h1 className="text-3xl font-extrabold tracking-tight">Operations Command Center</h1>
<p className="text-sm text-muted-foreground">Real-time system telemetry and infrastructure health.</p>
</header>
{/* 2. PPR Dynamic Boundary + Cache Slot:
- Fallback skeleton sent immediately in initial layout
- Dynamic hole streams from server cache in ~5ms for subsequent users
- Database query executes only ONCE every 60 seconds */}
<Suspense fallback={<MetricsSkeleton />}>
<DinouCacheSlot id="operations-telemetry" duration={60} tags={["telemetry"]}>
<HeavyMetricsSubtree />
</DinouCacheSlot>
</Suspense>
</main>
);
}Outer shell and fallback skeleton arrive at 0ms from CDN edge.
Caches the dynamic stream chunk on the server during the TTL window.
Call revalidateTag("telemetry") or refreshSlot() to update without full-page reloads.
6.6. When Should You Use <DinouCacheSlot>?
A common question in full-stack architecture is: Does <DinouCacheSlot> only make sense for dynamic components or subtrees?
The short answer: While dynamic routes and PPR are where it delivers exponential returns, it also unlocks unique capabilities for static pages (such as asymmetric revalidation and in-place client slot refreshes). Understanding when to use it boils down to the distinction between Horizontal Segmentation (route/layout-level) and Vertical Segmentation (component-level).
If an entire route is pre-rendered at build time with no revalidation, it is already cached as immutable HTML at the CDN Edge. Wrapping parts of it in a slot adds zero caching benefit.
Wrapping your entire page.tsx body inside a slot reinvents horizontal caching. Use export const revalidate = 300 in page_functions.ts instead.
Wrapping personal user data (e.g. private user carts or session tokens) with a static slot ID will cache that private data for the first user and leak it to subsequent visitors.
Dynamic pages (reading cookies, auth tokens, or search params) cannot be cached at the CDN or route level because each user receives a customized response. However, many widgets on that page (stock tickers, trending feeds, heavy SQL analytics) are identical across users. <DinouCacheSlot> isolates the expensive widget with its own TTL:
// src/routes/dashboard/page.tsx (Dynamic authenticated SSR)
import { Suspense } from "react";
import { DinouCacheSlot } from "dinou";
import { getSession } from "@/lib/auth";
import { GlobalMetricsWidget } from "./metrics";
export default async function DashboardPage() {
const session = await getSession(); // Evaluated dynamically per user
return (
<div className="p-6">
<h1>Welcome back, {session.user.name}</h1> {/* 100% Live & personal */}
{/* Global widget: cached for 10 minutes on the server across all users */}
<Suspense fallback={<div>Loading global telemetry...</div>}>
<DinouCacheSlot id="global-metrics" duration={600} tags={["metrics"]}>
<GlobalMetricsWidget />
</DinouCacheSlot>
</Suspense>
</div>
);
}In PPR, the static shell ships instantly from CDN (0ms TTFB), while dynamic holes stream in via React Suspense. Without a slot, 100,000 visitors trigger 100,000 database hits. Placing <DinouCacheSlot> inside the hole means only the first request queries the DB; the other 99,999 stream the cached RSC Flight payload in ~2-5ms:
This is where <DinouCacheSlot> proves it is not limited to dynamic pages. You can use it in static/ISR pages for two critical architectural advantages:
Suppose a product page has heavy media and specifications that rarely change (cached for 24 hours: revalidate = 86400). But live stock pricing fluctuates every 60 seconds. A slot lets the pricing widget expire every 60s without re-rendering the whole page!
A Client Component can trigger an on-demand re-render of only this Server Component using refreshSlot(slotId) without refreshing the browser page or losing client state (scroll position, inputs, audio):
// 1. In your Page (Server Component): Asymmetric slot inside static route
<DinouCacheSlot id={`price-${product.id}`} duration={60} tags={["pricing"]}>
<LivePriceCalculator productId={product.id} />
</DinouCacheSlot>
// 2. In an interactive Client Component: refresh ONLY that slot
"use client";
import { refreshSlot } from "dinou";
export function RefreshPriceButton({ productId }: { productId: string }) {
return (
<button onClick={() => refreshSlot(`price-${productId}`, { fresh: true })}>
Check Latest Stock & Price
</button>
);
}Quick Decision Checklist
<DinouCacheSlot> inside <Suspense> to protect your database and stream instantly.refreshSlot() to update just that server subtree in-place, use a slot.<DinouCacheSlot>. Let build-time SSG and CDN Edge caching handle the entire page with zero runtime overhead.6.7. Common Traps & Anti-Patterns to Avoid
Marking src/layout.tsx as a Client Component cascades the client boundary to every single page in your application. Root layouts should always be Server Components to protect secrets and enable streaming.
Never write import ServerComponent from "./ServerComponent" inside a file containing "use client". Use the Composition Pattern (Section 6.2) and pass it as children instead.
Calling window.innerWidth or localStorage.getItem() outside of a useEffect in a Client Component causes server pre-rendering crashes. Always wrap browser calls in useEffect or an event handler.
Do not pass callback functions or ORM model class instances with methods as props to Client Components. Extract plain JavaScript objects or use Server Functions ("use server") for actions.
7. Data Fetching & Rendering Strategies Matrix
To help you choose the ideal pattern for each route and component, the following matrix compares the data fetching and rendering strategies available in Dinou, their execution timing, TTFB, streaming resolution latency, database/server pressure, and network round-trip characteristics:
| # | Strategy / Pattern | Execution Context | TTFB (First Byte) | Stream / Content Ready | Server & DB Load | Network Waterfall? | Data Freshness | Ideal Use Case |
|---|---|---|---|---|---|---|---|---|
| 1 | RSC Direct await (SSG) Top-level await or getProps in Server Component | Build Time (npm run build) | 0ms (CDN Edge) | Instant (0ms) | Zero (Build Time) | None | Fixed at build | Blogs, documentation, static marketing pages. |
| 2 | RSC Direct await (Dynamic) Top-level await or getProps without <Suspense> | Per Request (Server) | Slow (Blocked) | N/A (Blocks TTFB) | Heavy (100% queries) | None | 100% Live | Private views where entire UI requires a single critical query and waiting is acceptable. |
| 3 | RSC with <Suspense> (SSG) Static route with async component boundary | Build Time (Compiler awaits) | 0ms (CDN Edge) | Instant (0ms) | Zero (Build Time) | None | Fixed at build | Decoupled async components in static pages; outputs final HTML without showing fallback. |
| 4 | RSC with <Suspense> (Dynamic) Streaming SSR on demand (without PPR) | Per Request (Server streaming) | Fast (Early Layout) | Slow (Waits 1-3s DB) | Heavy (100% queries) | None | 100% Live | Dynamic routes on runtimes without edge caching or when PPR is disabled. |
| 5 | RSC with <Suspense> + <DinouCacheSlot> (Static / Asymmetric ISR) Pre-rendered static shell with decoupled slot TTL or refreshSlot() | Build Time (Pre-rendered) + On-Demand Slot Reval | 0ms (CDN Edge) | Instant (~0-2ms) | Near-Zero (Decoupled) | None | Decoupled TTL / refreshSlot() | Static/ISR pages (e.g. 24h page revalidate) containing a widget (stock, pricing) needing fast updates (60s) or surgical in-place client refreshes. |
| 6 | RSC with <Suspense> + <DinouCacheSlot> (Dynamic) Dynamic streaming with vertical Micro-ISR (TTL cache) | Per Request (Cache hit: ~2ms / Miss: async query) | Fast (Early Layout) | Instant (~2ms Cache) | Near-Zero (99% Offload) | None | TTL / Tag Reval | Dynamic routes (with session/cookies) containing heavy widgets (analytics, feeds). Flushes layout instantly; slot streams from cache without DB load. |
| 7 | RSC with <Suspense> (PPR Hybrid) Partial Prerendering (Static Shell + Live Uncached Hole) | Static Shell (Build) + Live Hole (Server on every request) | 0ms (Shell from Edge) | Slow (Hole waits for DB) | Heavy (100% Hole queries) | None | 100% Live | Instant static layout from Edge with zero-delay live streamed data for real-time personalization. |
| 8 | PPR + <DinouCacheSlot> (PPR Hybrid Synergy)Synergy Static Edge Shell + Cached Streamed Hole (Max Concurrency) | Edge CDN (Shell) + Server Cache Storage (Slot) | 0ms (Edge) + ~5ms (Slot) | Instant (~2-5ms Cache) | Near-Zero (99.9% Offload) | None | TTL / Tag Reval | Peak Performance: Instant CDN paint (0ms TTFB) with 99.9% database query offload. Ideal for high-traffic stores, trending catalog pages, and viral portals. |
| 9 | RSC Promise ➔ Client use() (Dynamic / PPR) RSC initiates un-awaited Promise; Client unwraps via use() | Server starts query; Client suspends | 0ms (with PPR) | Waits for Promise | Heavy (Live query) | None | 100% Live | Interactive stateful widgets (charts, canvas, filters) needing fresh streaming data. |
| 10 | Client-First getProps (SSG) page_functions.ts + Client Component | Build Time (npm run build) | 0ms (CDN Edge) | Instant (0ms) | Zero (Build Time) | None | Fixed at build | Dinou Client-First model for highly interactive static web applications. |
| 11 | Client-First getProps (Dynamic) page_functions.ts + Client Component in SSR | Per Request (Server awaits getProps) | Slow (Blocked) | N/A (Blocks TTFB) | Heavy (100% queries) | None | 100% Live | Complex SPAs where developers prefer classic React hooks and tolerate TTFB wait. |
| 12 | Client Calling Server Function (Initial Mount - Dynamic RPC) Client Component calls "use server" during initial mount | Client RPC on mount | Fast (Shell Only) | Delayed (Waterfall) | Heavy (100% RPC queries) | Yes (Waterfall) | 100% Live | Supported, but sub-optimal compared to pattern 9 (RSC Promise streaming) due to the second HTTP round trip. |
| 13 | Client Calling Server Function (User Action - Dynamic RPC) Invoked on user button click, form submission, or mutation | Client on user action (POST RPC) | N/A (Post-Load) | N/A (User Event) | On-Demand (Per Action) | None | 100% Live | Form submissions, state persistence, on-demand data refreshes, and RPC actions. |
Server Components with getProps
getProps with a Server Component is functionally equivalent to doing top-level await in the component body (Patterns 1 & 2): both run on the server before rendering the component and block TTFB on dynamic routes unless wrapped in <Suspense>.Legacy useEffect Fetching Omitted
useEffect + fetch() is intentionally omitted from the primary strategies. In React 19 and modern RSC architectures, fetching on mount via useEffect is considered a legacy anti-pattern that creates network waterfalls, causes layout shifts, and misses server streaming.