Skip to content

Rendering Patterns Decision Framework

expert22 min read

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

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.

Quiz
A news site publishes 50 articles per day. Each article gets 100K views in the first hour, then traffic drops sharply. Which rendering pattern is most appropriate?

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
Quiz
A product page has a description (static), reviews list (static, data-heavy), and an Add to Cart button (interactive). With React Server Components, how much JavaScript ships to the client?

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:

  1. Qwik intercepts the click event via a global listener
  2. Downloads the click handler chunk (~1KB)
  3. Deserializes the component state from HTML
  4. Executes the handler
  5. Updates the DOM

No hydration step. No downloading the entire component tree. Just the handler for the specific interaction.

Common Trap

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.

Quiz
What is the fundamental difference between hydration (SSR) and resumability (Qwik)?

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

PatternContent DynamismPersonalizationSEOTTIInfra Cost
SSGLow (build-time)NoneExcellentFast (no hydration overhead beyond JS)Minimal (CDN)
ISRMedium (revalidate)None/URL-basedExcellentFast (cached)Low
SSRHigh (per-request)FullExcellentMedium (server + hydration)High (compute)
Streaming SSRHigh (per-request)FullExcellentBetter (progressive)High (compute)
RSCHigh (per-request)FullExcellentBest (minimal JS)Medium-High
CSRAny (client-fetched)FullPoorSlow (full JS bundle)Minimal
IslandsLow-MediumPer-islandExcellentFast (minimal JS)Low-Medium
ResumabilityAnyFullExcellentFastest (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.
Quiz
An enterprise SaaS application behind authentication has a complex dashboard with charts, real-time data tables, and form wizards. Which rendering pattern fits best?

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:

  1. The static shell of a page is prerendered at build time (SSG-fast)
  2. Dynamic "holes" are marked with Suspense boundaries
  3. On request, the static shell is served instantly from CDN
  4. 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.

Key Rules
  1. 1Rendering patterns are per-route decisions, not per-application -- different routes have different needs
  2. 2The five decision dimensions: content dynamism, personalization, SEO, TTI budget, infrastructure cost
  3. 3SSG for static content, SSR/streaming for dynamic, CSR for rich interactivity behind auth, islands/resumability for minimal-JS pages
  4. 4RSC and PPR are convergence patterns that combine static and dynamic rendering in a single page
  5. 5Resumability (Qwik) trades hydration cost for per-interaction lazy loading -- best when first-load speed matters more than first-interaction speed
What developers doWhat 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