DumbPhobia#007 Web Rendering Strategies

Choosing the wrong rendering strategy is one of the most expensive architectural mistakes you can make — it affects performance, SEO, infrastructure cost, and developer experience. Here's everything you need to know to choose the right one.

What is a Rendering Strategy?

A rendering strategy defines where and when your HTML is generated. Every web application has to answer:

  • Where does the HTML come from — the server, the client, or a CDN?
  • When is it generated — at build time, at request time, or in the browser?

The answer to those two questions determines your performance characteristics, your infrastructure requirements, and the kinds of content your app can support.

There are five major strategies in modern frontend development:

StrategyFull NameWhereWhen
SSGStatic Site GenerationServer (build)Build time
ISRIncremental Static RegenerationServer (build + runtime)Build time + on demand
SSRServer-Side RenderingServer (runtime)Each request
CSRClient-Side RenderingBrowserRuntime
PPRPartial Pre-RenderingServer + BrowserBuild time + Runtime

The Critical Rendering Path — A Quick Baseline

Before comparing strategies, it helps to understand what the browser is doing. To show a pixel on screen, the browser must:

HTML received → Parse DOM → Fetch CSS/JS → Build CSSOM → Render tree → Paint

Every rendering strategy is essentially an optimization of when and how much of this work is done ahead of time. The more pre-work done before the user's request, the faster the first paint.


1. SSG — Static Site Generation

How it works

The entire application is rendered to HTML at build time — before any user requests it. The resulting static HTML files are deployed to a CDN and served instantly to any user anywhere in the world.

Build time:
  Source code + data → [Build process] → Static HTML/CSS/JS files → CDN

Request time:
  User requests page → CDN serves pre-built HTML → Browser displays instantly

Implementation (Next.js)

// Page is generated at build time
export default function BlogPost({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

// Tell Next.js which paths to pre-render
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map(post => ({ slug: post.slug }));
}

// Fetch data at build time
export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);
  return { title: post.title };
}

Performance profile

First Contentful Paint (FCP)    → Excellent — HTML is pre-built and CDN-cached
Time to First Byte (TTFB)       → Excellent — served from edge, no server processing
Largest Contentful Paint (LCP)  → Excellent — content is in the initial HTML
Interaction to Next Paint (INP) → Depends on client-side JS hydration
Time to Interactive (TTI)       → Depends on JS bundle size

Use cases

  • Marketing and landing pages
  • Documentation sites (like React docs, MDN)
  • Blogs and editorial content
  • Product listing pages where content changes infrequently
  • Portfolio sites

Pros

  • Fastest possible load time — HTML is pre-built and edge-cached
  • No server required — can deploy to any static host (Vercel, Netlify, GitHub Pages, S3)
  • Infinitely scalable — CDN handles all traffic, no origin server under load
  • Excellent SEO — full HTML in the initial response, no JS required for crawlers
  • Maximum reliability — no runtime failures, no database timeouts

Cons

  • Stale content — data is baked in at build time; changing it requires a rebuild
  • Long build times at scale — thousands of pages means a slow build pipeline
  • Not suitable for personalized content — everyone gets the same HTML
  • No access to request-time data — can't read cookies, headers, or user sessions at render time

When to use SSG

✓ Content changes rarely (daily or less)
✓ Content is the same for every user (not personalized)
✓ Maximum performance and SEO are the top priorities
✓ You want to minimize infrastructure costs
✗ Content changes frequently (use ISR or SSR instead)
✗ Content is user-specific or personalized

2. ISR — Incremental Static Regeneration

How it works

ISR extends SSG by adding the ability to regenerate individual static pages after deploy without rebuilding the entire site. Pages are still served from a CDN cache, but the cache is invalidated and regenerated on a schedule or on demand.

Build time:
  Source code + data → Static HTML files → CDN

Request time (cache hit):
  User requests page → CDN serves cached HTML → (background: check if stale)

Request time (cache miss or stale):
  User requests page → CDN serves old cached HTML → Server regenerates → CDN updates cache

The key behavior: the first user after expiry still gets the old page (stale-while-revalidate), while the new version is being generated in the background. The next user gets the fresh version.

Implementation (Next.js)

// Revalidate this page every 60 seconds
export const revalidate = 60;

export default function ProductPage({ product }) {
  return (
    <main>
      <h1>{product.name}</h1>
      <p>Price: ${product.price}</p>
      <p>Stock: {product.stock}</p>
    </main>
  );
}

// On-demand revalidation — trigger from a webhook or CMS
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';

export async function POST(request: Request) {
  const { path, secret } = await request.json();
  if (secret !== process.env.REVALIDATION_SECRET) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }
  revalidatePath(path);
  return Response.json({ revalidated: true });
}

Time-based vs. on-demand revalidation

ModeHow it worksBest for
Time-basedPage regenerates after N secondsAcceptable staleness, no CMS integration
On-demandWebhook triggers regeneration on content changeCMS-driven content, zero acceptable staleness

Performance profile

First Contentful Paint (FCP)    → Excellent — still served from CDN cache
Time to First Byte (TTFB)       → Excellent — CDN edge serving
Largest Contentful Paint (LCP)  → Excellent
Data freshness                  → Good — stale-while-revalidate, up to N seconds stale

Use cases

  • E-commerce product pages (prices and stock change, but not per-request)
  • News articles (publish once, rarely edited)
  • CMS-driven content (regenerate on webhook from headless CMS)
  • Event pages and listings
  • Any SSG use case where content changes more than once a day

Pros

  • Near-SSG performance — still served from CDN cache
  • Fresh content without full rebuilds — update one page without touching others
  • On-demand revalidation — CMS webhooks trigger regeneration immediately
  • Scales like SSG — CDN absorbs traffic, not your origin server

Cons

  • Stale-while-revalidate behavior — first user after expiry gets old content
  • More complex than SSG — requires understanding of cache invalidation
  • Still not suitable for personalized content
  • On-demand revalidation requires a server endpoint — not purely static anymore
  • Revalidation latency — there's still a window where content is stale

When to use ISR

✓ Content changes periodically (hourly, daily) but not per-request
✓ You want SSG performance but can't afford stale data for long
✓ You have a headless CMS with webhook support
✓ E-commerce catalog with frequently changing prices or inventory
✗ Content must be completely fresh on every request (use SSR)
✗ Content is personalized per user (use SSR or CSR)

3. SSR — Server-Side Rendering

How it works

The HTML is generated on the server for every incoming request. The server fetches the latest data, renders the full HTML, and sends it to the client. The client receives a fully-formed page with real, up-to-date content.

Request time (every request):
  User requests page
    → Server receives request (reads cookies, headers, session)
    → Server fetches fresh data from database/API
    → Server renders HTML with that data
    → Server sends full HTML to client
    → Browser displays content
    → Client-side JS hydrates for interactivity

Implementation (Next.js)

// This component runs on the server on every request
export default async function Dashboard({ searchParams }) {
  // Access request-time data — user session, query params
  const session = await getSession();
  if (!session) redirect('/login');

  // Fetch fresh, personalized data
  const [user, feed, notifications] = await Promise.all([
    getUser(session.userId),
    getFeed(session.userId, searchParams.cursor),
    getNotifications(session.userId),
  ]);

  return (
    <main>
      <h1>Welcome back, {user.displayName}</h1>
      <NotificationBadge count={notifications.unread} />
      <NewsFeed posts={feed.posts} nextCursor={feed.nextCursor} />
    </main>
  );
}

How SSR differs from SSG/ISR

SSG:  Build time  → data fetched once → HTML cached forever
ISR:  Build time  → data fetched once → HTML cached for N seconds
SSR:  Request time → data fetched fresh → HTML never cached (or short TTL)

The hydration step

SSR sends pre-rendered HTML, which the user sees immediately. But the page is initially non-interactive — it's static HTML. React then downloads and runs the JavaScript bundle to hydrate the page, attaching event listeners and making it interactive.

User sees content (SSR HTML)   →  JS downloads  →  JS hydrates  →  Interactive
        |                                                                |
   ~100-300ms                                                     ~500-2000ms
   (fast, great FCP)                                          (depends on bundle size)

This gap between visible content and interactivity is called the hydration gap — a key SSR challenge.

Performance profile

First Contentful Paint (FCP)    → Good — full HTML in first response
Time to First Byte (TTFB)       → Slower — server must process before responding
Largest Contentful Paint (LCP)  → Good — content in initial HTML
Time to Interactive (TTI)       → Depends on JS bundle and hydration time
Server load                     → High — every request hits the server

Use cases

  • Authenticated dashboards and user feeds
  • Personalized pages (recommendations, account settings)
  • Pages that require access to cookies, auth tokens, or session data
  • Real-time or highly dynamic content
  • E-commerce checkout and cart pages
  • Search results pages

Pros

  • Always fresh data — fetched on every request, never stale
  • Access to request context — cookies, headers, auth tokens, IP address
  • Full personalization — every user gets their own rendered HTML
  • Good SEO — full HTML in the initial response (unlike CSR)
  • No client-side data fetching waterfalls — server fetches in parallel

Cons

  • Slower TTFB — server must do work before responding
  • High server load — every page view hits the server
  • Hydration gap — page is visually ready but not interactive until JS loads
  • Hydration errors — server and client render must match exactly
  • Expensive to scale — more traffic = more server capacity needed
  • Not CDN-cacheable by default — dynamic responses can't be edge-cached

When to use SSR

✓ Content is personalized per user
✓ You need access to cookies, auth tokens, or request headers
✓ Data changes on every request or must be completely fresh
✓ SEO matters AND content is dynamic
✗ Content is the same for all users (use SSG/ISR instead)
✗ Interactivity is more important than initial load (consider CSR)
✗ You can't afford server infrastructure (use SSG/ISR)

4. CSR — Client-Side Rendering

How it works

The server sends a minimal HTML shell — essentially an empty <div id="root"> — and the browser does all the rendering work. JavaScript downloads, executes, fetches data from APIs, and renders the UI entirely in the browser.

Request time:
  User requests page
    → Server sends empty HTML shell + JS bundle links
    → Browser downloads and parses HTML (sees blank page)
    → Browser downloads JS bundle
    → JavaScript executes, React initializes
    → React fetches data from API
    → React renders UI with data
    → User sees content

Implementation

// The HTML file the server sends
// <div id="root"></div>  ← this is all the server sends

// React takes over from here
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/dashboard" element={<Dashboard />} />
      </Routes>
    </BrowserRouter>
  );
}

// Data fetching happens client-side
function Dashboard() {
  const { data, isLoading } = useQuery({
    queryKey: ['dashboard'],
    queryFn: () => api.get('/dashboard'),
  });

  if (isLoading) return <DashboardSkeleton />;
  return <DashboardLayout data={data} />;
}

The CSR loading waterfall

CSR has an inherent sequential loading problem:

1. Request HTML         → receive empty shell
2. Download JS bundle   → wait for network
3. Parse and execute JS → CPU work
4. Fetch data from API  → wait for network again
5. Render UI            → user finally sees something

Each step blocks the next. On slow connections or low-end devices, this results in a significant delay before the user sees anything — the white screen of death.

Performance profile

First Contentful Paint (FCP)    → Poor — blank screen until JS loads
Time to First Byte (TTFB)       → Excellent — server sends HTML immediately
Largest Contentful Paint (LCP)  → Poor — content requires JS execution + API call
Time to Interactive (TTI)       → Good — once rendered, already interactive
SEO                             → Poor — crawlers see empty HTML
Server load                     → Very low — server just serves static files

Use cases

  • Authenticated applications where SEO is not a concern (dashboards, admin tools, SaaS products behind login)
  • Highly interactive, stateful UIs (rich text editors, design tools, data visualization dashboards)
  • Applications with real-time data that updates constantly
  • Internal tools and B2B applications
  • Progressive Web Apps (PWAs)

Pros

  • Simplest to develop — no server-client boundary to reason about
  • Rich interactivity — full client-side state management without hydration constraints
  • Minimal server infrastructure — just a CDN for static files
  • Instant subsequent navigation — client-side routing is seamless after initial load
  • No hydration issues — rendering only happens once, on the client

Cons

  • Worst initial load performance — blank screen until JS executes
  • Poor SEO — search crawlers typically see empty HTML
  • JavaScript-dependent — broken experience if JS fails or is blocked
  • Data fetching waterfalls — serial network requests compound delay
  • Large bundle sizes hit users hard on slow connections

When to use CSR

✓ Application is behind authentication (SEO irrelevant)
✓ Highly interactive UI where rich client-side state is needed
✓ Internal tools, dashboards, admin panels
✓ You want to minimize server infrastructure
✗ SEO is important (use SSG, ISR, or SSR instead)
✗ First load performance is critical
✗ Users are on slow or unreliable networks

5. PPR — Partial Pre-Rendering

How it works

PPR is the newest strategy, introduced in Next.js 14+. It combines the best of SSG and SSR on the same page: static parts are pre-rendered and CDN-cached at build time, while dynamic holes are streamed from the server at request time.

The browser receives a static HTML shell instantly from the CDN. The dynamic parts are streamed in as they become available — wrapped in <Suspense> boundaries.

Build time:
  Static shell → pre-rendered → CDN cached

Request time:
  User requests page
    → CDN serves static shell instantly (fast TTFB)
    → Server streams dynamic parts as they resolve
    → Browser progressively renders dynamic content
    → Full page interactive after hydration
Timeline:
  0ms    → CDN sends static shell (header, layout, skeleton UI)
  50ms   → Browser paints static content
  200ms  → Server resolves user data → streams personalized section
  400ms  → Server resolves feed data → streams feed content
  600ms  → All dynamic content loaded → page fully interactive

Implementation (Next.js 14+)

// Enable PPR for this route
export const experimental_ppr = true;

import { Suspense } from 'react';

// Static shell — pre-rendered at build time
export default function Page() {
  return (
    <main>
      {/* Static — pre-rendered, served from CDN instantly */}
      <Header />
      <HeroSection />
      <NavigationBar />

      {/* Dynamic — streamed from server at request time */}
      <Suspense fallback={<UserGreetingSkeleton />}>
        <UserGreeting />       {/* reads user session → dynamic */}
      </Suspense>

      <Suspense fallback={<FeedSkeleton />}>
        <PersonalizedFeed />   {/* reads user preferences → dynamic */}
      </Suspense>

      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations />    {/* ML recommendations → dynamic */}
      </Suspense>

      {/* Static — pre-rendered */}
      <Footer />
    </main>
  );
}

// This component runs dynamically on the server per request
async function UserGreeting() {
  const user = await getCurrentUser(); // reads cookies
  return <h2>Welcome back, {user.name}!</h2>;
}

// This component is pre-rendered at build time
function HeroSection() {
  return <div className="hero">Discover something new today</div>;
}

How PPR compares to streaming SSR

PPR is often confused with streaming SSR — they're related but different:

Streaming SSRPPR
Static shell sourceGenerated server-side per requestPre-built, CDN-cached
TTFBDepends on serverNear-instant (CDN)
Dynamic partsStreamed from serverStreamed from server
CacheabilityLimitedStatic shell is fully cached

PPR gets the CDN caching benefits of SSG for the static shell, while streaming SSR still generates the shell server-side on each request.

Performance profile

First Contentful Paint (FCP)    → Excellent — static shell from CDN instantly
Time to First Byte (TTFB)       → Excellent — CDN serves static shell
Largest Contentful Paint (LCP)  → Excellent (if LCP element is in static shell)
Time to Interactive (TTI)       → Good — hydration of streamed parts
Personalization                 → Full — dynamic holes are per-request
SEO                             → Excellent — full content eventually rendered

Use cases

  • High-performance e-commerce (static product layout, dynamic price/stock/recommendations)
  • News and media platforms (static article, dynamic comments/reactions)
  • Authenticated home feeds (static shell, dynamic personalized feed)
  • Any page that has a mix of static and personalized content
  • Applications where both maximum performance and personalization are required

Pros

  • Best of SSG and SSR combined — static speed + dynamic content
  • Instant static shell from CDN — fastest possible perceived load
  • Progressive rendering — content streams in as it's ready, no blank screen
  • Granular control — decide per <Suspense> boundary what's static vs. dynamic
  • Great SEO — content is server-rendered, not client-side JS

Cons

  • Most complex strategy — requires understanding of Suspense, streaming, and hydration
  • Framework-dependent — currently Next.js 14+ only (experimental)
  • Suspense boundary design — requires careful component architecture
  • Harder to test — static and dynamic parts behave differently in tests
  • Debugging is harder — interleaved static/dynamic rendering is non-trivial to trace

When to use PPR

✓ Page has a clear mix of static layout and dynamic/personalized content
✓ Both performance (LCP) and personalization are non-negotiable
✓ You're using Next.js 14+ and willing to use experimental features
✓ E-commerce, media, or high-traffic authenticated apps
✗ Entirely static content (use SSG — simpler)
✗ Entirely dynamic/personalized content (use SSR — simpler)
✗ You need broad framework support beyond Next.js right now

Comparing All Five Strategies

Performance comparison

MetricSSGISRSSRCSRPPR
TTFB⚡ Excellent⚡ Excellent🟡 Moderate⚡ Excellent⚡ Excellent
FCP⚡ Excellent⚡ Excellent🟢 Good🔴 Poor⚡ Excellent
LCP⚡ Excellent⚡ Excellent🟢 Good🔴 Poor⚡ Excellent
TTI🟡 Moderate🟡 Moderate🟡 Moderate🟢 Good🟡 Moderate
INP🟢 Good🟢 Good🟢 Good🟢 Good🟢 Good

Feature comparison

FeatureSSGISRSSRCSRPPR
Fresh data❌ Build time only🟡 Periodic / on-demand✅ Every request✅ Every request✅ Dynamic parts
Personalization✅ Full✅ Full✅ Dynamic parts
SEO
CDN cacheable✅ Full✅ Full✅ Static files✅ Static shell
Server required🟡 For revalidation✅ For dynamic parts
Build time cost🟡 High at scale🟢 Low✅ None✅ None🟢 Low
Complexity🟢 Low🟡 Medium🟡 Medium🟢 Low🔴 High

Decision Framework

Use this flowchart to choose the right strategy for any given page:

Is the content the same for every user?
│
├── YES → Does it change frequently?
│          │
│          ├── NO (rarely changes)
│          │    → SSG
│          │
│          └── YES (changes hourly/daily)
│               → ISR (time-based or on-demand)
│
└── NO (personalized per user)
     │
     ├── Does the page have a significant static layout?
     │    │
     │    ├── YES → Use PPR
     │    │         (static shell from CDN + dynamic parts streamed)
     │    │
     │    └── NO (mostly dynamic)
     │         → SSR
     │
     └── Is SEO required?
          │
          ├── YES → SSR (or PPR)
          │
          └── NO (behind auth, internal tool)
               → CSR

Mixing Strategies in One Application

Modern frameworks let you choose a strategy per page or per route — and you should. A single app might use all five:

Marketing site      /               → SSG
Blog posts          /blog/:slug     → SSG
News articles       /news/:slug     → ISR (revalidate every 5 min)
Search results      /search         → SSR (query-dependent)
Product pages       /products/:id   → ISR (price/stock changes)
Checkout            /checkout       → SSR (cart + auth + real-time stock)
User dashboard      /dashboard      → PPR (static shell + dynamic feed)
Admin panel         /admin/*        → CSR (authenticated, SEO irrelevant)
Chat feature        /messages       → CSR (real-time, no SSR needed)

Common Mistakes

Defaulting to SSR for everything: SSR seems "safe" because it's always fresh, but it's expensive. Static or ISR pages serve faster at lower cost.

Using CSR for public, SEO-sensitive pages: A public product page rendered entirely in JavaScript will never rank as well as a server-rendered equivalent.

Ignoring the hydration gap in SSR: SSR gives you fast FCP but not fast TTI. A large JavaScript bundle means users see the page but can't interact with it for several seconds.

Not using ISR when you should: Teams building with SSG often do full rebuilds when a single page changes. ISR eliminates this — only the affected page regenerates.

Over-engineering with PPR too early: PPR is powerful but complex. If a page can be handled cleanly by SSR or SSG, use the simpler option.


Quick Reference Card

SSG — Static Site Generation
  ├─ When:  Build time
  ├─ Where: CDN (fully cached)
  ├─ Data:  Fetched at build, baked into HTML
  ├─ Best:  Marketing, docs, blogs, public catalogs
  └─ Avoid: Personalized or frequently-changing content

ISR — Incremental Static Regeneration
  ├─ When:  Build time + background regeneration
  ├─ Where: CDN (cached with expiry)
  ├─ Data:  Refreshed periodically or on webhook
  ├─ Best:  E-commerce, CMS content, news articles
  └─ Avoid: Data that must be completely fresh per request

SSR — Server-Side Rendering
  ├─ When:  Every request
  ├─ Where: Origin server
  ├─ Data:  Fetched fresh on every request
  ├─ Best:  Auth pages, personalized dashboards, search
  └─ Avoid: High-traffic static content (use SSG/ISR)

CSR — Client-Side Rendering
  ├─ When:  In the browser after JS loads
  ├─ Where: Browser
  ├─ Data:  Fetched client-side via API calls
  ├─ Best:  Authenticated apps, admin tools, SPAs
  └─ Avoid: Public pages where SEO or FCP matters

PPR — Partial Pre-Rendering
  ├─ When:  Build time (shell) + request time (dynamic parts)
  ├─ Where: CDN (shell) + server (dynamic holes)
  ├─ Data:  Static content pre-baked, dynamic content streamed
  ├─ Best:  Mixed pages needing both speed and personalization
  └─ Avoid: Simple pages that fit cleanly into SSG or SSR

No single rendering strategy wins across all dimensions. The best frontend architects don't pick one — they pick the right one for each route, based on the content's freshness requirements, personalization needs, SEO importance, and performance targets. That's the judgment that separates senior engineers from the rest.