Rendering Patterns Decision Framework
The Rendering Pattern Zoo
Frontend rendering in 2026 isn't "SSR or SPA?" anymore. It's a spectrum of at least eight distinct patterns, each with real trade-offs. Pick wrong and you're either serving stale content, shipping too much JavaScript, or paying for infrastructure you don't need.
This guide gives you a decision framework -- not "use X because it's trendy," but "here's what your app actually needs, and here's the pattern that fits."
The Mental Model
Think of rendering patterns as food delivery options:
- SSG is a frozen meal: prepared in advance, stored in the freezer (CDN), microwaved (served) instantly. Zero wait time, but can't customize per customer.
- SSR is a cook-to-order restaurant: every request gets a fresh meal (page) cooked on the spot. Always fresh, but the kitchen (server) needs to handle every order.
- CSR is a meal kit delivery: the kitchen sends raw ingredients (JavaScript bundle) and a recipe. The customer (browser) cooks it themselves. The kitchen is free, but the customer waits while cooking.
- Streaming SSR is the restaurant serving courses as they're ready: the appetizer (shell) arrives immediately while the main course (data-heavy content) is still cooking.
- RSC is a chef who preps everything in the kitchen and only sends plated dishes: no raw ingredients (JavaScript) leave the kitchen unless they need to be reheated (hydrated) at the table.
- Islands architecture is a buffet where most dishes are pre-plated (static HTML), but a few stations have live cooking (interactive components).
- Resumability (Qwik) is a meal that arrives fully cooked but with a microwave instruction card: eat it cold (static HTML), and if you want it warm (interactive), the microwave only heats the specific portion you're about to eat.
The Patterns
CSR (Client-Side Rendering)
Browser loads HTML shell → Downloads JS bundle → Executes JS → Renders UI → Interactive
The original SPA pattern. The server sends an empty HTML shell with a script tag. All rendering happens in the browser.
Characteristics:
- Time to First Byte (TTFB): fast (tiny HTML)
- First Contentful Paint (FCP): slow (waits for JS download + execution)
- Time to Interactive (TTI): slow (same as FCP)
- SEO: poor (empty HTML, requires JS rendering)
- Server cost: minimal (static file hosting)
Best for: Internal tools, dashboards, apps behind auth where SEO doesn't matter and users have good connections.
SSR (Server-Side Rendering)
Browser requests page → Server renders HTML → Sends full HTML → Browser shows content → Downloads JS → Hydrates → Interactive
The server generates complete HTML on every request. The browser shows content immediately, then downloads JavaScript to make it interactive (hydration).
Characteristics:
- TTFB: slower (server compute per request)
- FCP: fast (full HTML arrives)
- TTI: delayed by hydration (JS download + execution before interactive)
- SEO: excellent (full HTML)
- Server cost: high (compute per request, scales with traffic)
Best for: Dynamic, personalized content that needs SEO. E-commerce product pages, social media feeds, search results.
SSG (Static Site Generation)
Build time: Render all pages → Deploy to CDN
Request time: CDN serves pre-built HTML → Downloads JS → Hydrates → Interactive
Pages are rendered at build time and served as static files from a CDN. Blazing fast but content is fixed until the next build.
Characteristics:
- TTFB: fastest (CDN edge)
- FCP: fastest (pre-built HTML)
- TTI: delayed by hydration
- SEO: excellent
- Server cost: near-zero (static hosting)
Best for: Content that changes infrequently. Documentation, blogs, marketing sites, landing pages.
ISR (Incremental Static Regeneration)
First request: Serve stale page from cache → Trigger background regeneration
Subsequent requests: Serve newly generated page
ISR bridges SSG and SSR. Pages are statically generated but can be revalidated in the background at a configurable interval. You get CDN speed with near-real-time freshness.
// Next.js ISR
export const revalidate = 60; // Revalidate at most every 60 seconds
Characteristics:
- TTFB: fast (cached)
- FCP: fast (pre-built HTML)
- Freshness: configurable (seconds to hours)
- Server cost: low (regeneration only on stale cache)
Streaming SSR
Server starts rendering → Sends HTML shell immediately → Streams content chunks as they resolve → Browser progressively renders
Instead of waiting for the entire page to render, the server sends the HTML shell first, then streams additional content as it becomes available. Users see a loading skeleton that fills in progressively.
// Next.js streaming with Suspense
export default function Page() {
return (
<main>
<Header /> {/* Sent immediately */}
<Suspense fallback={<Skeleton />}>
<SlowDataComponent /> {/* Streamed when data resolves */}
</Suspense>
<Footer /> {/* Sent immediately */}
</main>
);
}
Key advantage: TTFB is as fast as sending a static shell. Slow data fetches don't block the entire page.
RSC (React Server Components)
Server renders component tree → Sends RSC payload (not HTML) → Client renders static parts, hydrates interactive parts
RSC is React's answer to "most of your page doesn't need JavaScript." Server Components render on the server and send their output as a serialized tree. They never ship to the client bundle. Only components marked 'use client' send JavaScript.
// Server Component (default in Next.js App Router)
async function ProductPage({ id }) {
const product = await db.products.get(id); // Direct DB access on server
return (
<div>
<h1>{product.name}</h1> {/* Static, no JS shipped */}
<p>{product.description}</p> {/* Static, no JS shipped */}
<AddToCart id={id} /> {/* 'use client': JS shipped for interactivity */}
</div>
);
}
Characteristics:
- Bundle size: drastically reduced (only interactive components ship JS)
- Data fetching: direct server access (DB, filesystem, APIs) without API routes
- Streaming: built-in with Suspense
- Composition: server and client components compose naturally
Islands Architecture
Server renders full HTML → Browser loads only JavaScript for interactive "islands"
Islands architecture (Astro, Fresh) renders the entire page as static HTML on the server, then selectively hydrates only the interactive portions ("islands"). Non-interactive content stays as plain HTML forever -- no JavaScript, no hydration.
---
// Astro: everything is static by default
import Header from './Header.astro'; // Static, no JS
import SearchBar from './SearchBar'; // Interactive island
---
<Header />
<SearchBar client:visible /> <!-- Hydrates only when visible in viewport -->
<article>{content}</article> <!-- Static, no JS ever -->
Key differentiator from RSC: Islands explicitly separate static and interactive content at the architectural level. RSC does this implicitly through the 'use client' boundary. Islands frameworks give you explicit hydration directives (client:load, client:visible, client:idle).
Resumability (Qwik)
Server renders HTML + serializes state → Browser serves static HTML instantly → On user interaction, loads only the handler code for that specific interaction
Qwik's resumability takes a fundamentally different approach to hydration. Instead of downloading all JavaScript and re-executing component trees to attach event handlers, Qwik serializes the application state into HTML and lazy-loads event handlers on demand.
// Qwik component
export const Counter = component$(() => {
const count = useSignal(0);
return (
<button onClick$={() => count.value++}>
Count: {count.value}
</button>
);
});
The $ suffix tells Qwik's optimizer to extract that function into a separate lazy-loaded chunk. When the user clicks the button:
- Qwik intercepts the click event via a global listener
- Downloads the click handler chunk (~1KB)
- Deserializes the component state from HTML
- Executes the handler
- Updates the DOM
No hydration step. No downloading the entire component tree. Just the handler for the specific interaction.
Resumability sounds like a silver bullet, but it trades initial load speed for interaction latency. The first click on any component requires a network request to download the handler code. On slow connections, this "lazy loading on interaction" can feel sluggish compared to pre-hydrated components. Qwik mitigates this with speculative prefetching (downloading likely-needed handlers during idle time), but the trade-off exists. For apps where first-load speed is more important than first-interaction speed (landing pages, content sites), it's a great trade-off. For apps where instant interactivity is critical (editors, games), it's not.
The Decision Framework
Choosing a rendering pattern isn't about technology -- it's about your constraints. Here are the five dimensions that determine the right choice:
Decision Matrix
| Pattern | Content Dynamism | Personalization | SEO | TTI | Infra Cost |
|---|---|---|---|---|---|
| SSG | Low (build-time) | None | Excellent | Fast (no hydration overhead beyond JS) | Minimal (CDN) |
| ISR | Medium (revalidate) | None/URL-based | Excellent | Fast (cached) | Low |
| SSR | High (per-request) | Full | Excellent | Medium (server + hydration) | High (compute) |
| Streaming SSR | High (per-request) | Full | Excellent | Better (progressive) | High (compute) |
| RSC | High (per-request) | Full | Excellent | Best (minimal JS) | Medium-High |
| CSR | Any (client-fetched) | Full | Poor | Slow (full JS bundle) | Minimal |
| Islands | Low-Medium | Per-island | Excellent | Fast (minimal JS) | Low-Medium |
| Resumability | Any | Full | Excellent | Fastest (near-zero JS) | Medium |
Concrete Recommendations
Documentation site / Blog / Marketing pages
- Pick: SSG (Astro, Next.js static export)
- Why: Content changes infrequently. CDN delivery is fastest and cheapest. No personalization needed.
E-commerce product pages
- Pick: ISR or streaming SSR with RSC
- Why: Products change (price, stock), but not per-second. ISR caches with revalidation. RSC minimizes JavaScript for product descriptions while keeping Add to Cart interactive.
Social media feed / News feed
- Pick: Streaming SSR or RSC
- Why: Content is personalized and real-time. Streaming shows the shell immediately while data loads. RSC keeps the feed items as server components (no JS for text/images).
Internal dashboard / Admin panel
- Pick: CSR (React SPA)
- Why: No SEO needed. Users are authenticated. Rich interactivity (charts, tables, forms). Can preload on login.
Content-heavy site with interactive widgets
- Pick: Islands (Astro) or RSC (Next.js)
- Why: Most content is static HTML. Only specific widgets need JavaScript. Islands or RSC minimize JS shipped.
High-traffic landing page (mobile-first)
- Pick: Resumability (Qwik) or Islands (Astro)
- Why: First-load speed drives conversions. Near-zero JavaScript means instant TTI on slow mobile connections.
Hybrid Patterns: The Real World
In practice, most applications use multiple patterns on different routes:
/ → SSG (marketing landing page)
/docs/* → SSG (documentation)
/blog/* → ISR (blog posts, revalidate daily)
/products/:id → Streaming SSR + RSC (dynamic, personalized pricing)
/dashboard/* → CSR (behind auth, rich interactivity)
/api/* → API routes (server-only)
Next.js App Router supports this naturally -- each route can opt into its own rendering strategy through data fetching patterns and caching configuration. Astro supports this with per-page rendering modes. The key insight: rendering patterns are per-route decisions, not per-application decisions.
Partial Prerendering (PPR): the convergence pattern
Next.js introduced Partial Prerendering (PPR) as the convergence of SSG and streaming SSR. With PPR:
- The static shell of a page is prerendered at build time (SSG-fast)
- Dynamic "holes" are marked with Suspense boundaries
- On request, the static shell is served instantly from CDN
- Dynamic holes are filled by streaming SSR on-demand
export default function ProductPage({ id }) {
return (
<div>
<Header /> {/* Static shell: pre-built */}
<ProductInfo id={id} /> {/* Static: pre-built at build time */}
<Suspense fallback={<PriceSkeleton />}>
<PersonalizedPrice id={id} /> {/* Dynamic hole: streamed */}
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<RecentReviews id={id} /> {/* Dynamic hole: streamed */}
</Suspense>
<Footer /> {/* Static shell: pre-built */}
</div>
);
}PPR gives you CDN-speed initial load (the static parts) with SSR-fresh dynamic content (the Suspense holes), in a single unified model. It's the closest thing to "have your cake and eat it too" in rendering patterns.
- 1Rendering patterns are per-route decisions, not per-application -- different routes have different needs
- 2The five decision dimensions: content dynamism, personalization, SEO, TTI budget, infrastructure cost
- 3SSG for static content, SSR/streaming for dynamic, CSR for rich interactivity behind auth, islands/resumability for minimal-JS pages
- 4RSC and PPR are convergence patterns that combine static and dynamic rendering in a single page
- 5Resumability (Qwik) trades hydration cost for per-interaction lazy loading -- best when first-load speed matters more than first-interaction speed
| What developers do | What they should do |
|---|---|
| Using SSR for everything because it is the safest option SSR has per-request server compute cost. Serving a documentation page via SSR when SSG would work wastes server resources and adds latency | Use SSG/ISR for content that does not change per-request, SSR only for truly dynamic/personalized content |
| Choosing CSR for a public-facing content site CSR sends an empty HTML shell. Search engines may not fully render JavaScript content, and users see a blank page until the JS bundle downloads and executes | Use SSG, ISR, or SSR for any page that needs SEO or fast first-contentful paint |
| Treating islands architecture and RSC as the same thing Islands (Astro) render pages as static HTML with opt-in hydration per component. RSC (Next.js) keeps everything in React's component model but renders most components on the server. The developer experience and composition model differ significantly | Islands explicitly separate static pages from interactive widgets. RSC separates server and client components within a unified React tree |