HomeCurriculumReact/Next.jsServer-Side Rendering (SSR) & Static Generation (SSG)
Server-Side Rendering (SSR) & Static Generation (SSG)

Server-Side Rendering (SSR) & Static Generation (SSG)

Analyze web rendering paradigms, deploy Incremental Static Regeneration (ISR), diagnose and cure hydration mismatches, and inspect React Server Component flight streaming payloads.

DT
Authored by DevThesis Systems Editorial Team
Reviewed for Technical Accuracy & Code Determinism

Rendering is the foundational mechanism by which web applications transform application state, database records, and component templates into physical HTML, CSS, and interactive DOM structures. Over the last two decades, web architectures have cycled from server-rendered monolithic templates (PHP, Rails) to completely client-side Single Page Applications (CSR in early React and Angular), and finally into the modern unified hybrid model pioneered by Next.js.

Modern high-performance web engineering does not treat rendering as a binary choice between "server" and "client." Instead, it treats rendering as a multi-dimensional spectrum encompassing Static Site Generation (SSG), Incremental Static Regeneration (ISR), Dynamic Server-Side Rendering (SSR), and Streaming SSR with Selective Hydration.

This guide provides an exhaustive, university-grade architectural analysis of modern rendering strategies in Next.js 15. We formulate mathematical models for Core Web Vitals (TTFB, FCP, LCP, TTI), dissect HTTP Chunked Transfer Encoding and out-of-order streaming, analyze the ISR state machine, diagnose hydration mismatch failures at the DOM level, and solve four comprehensive quantitative checkpoint problems.


1. The Rendering Taxonomy: CSR, SSR, SSG & ISR#

Every rendering strategy makes distinct architectural trade-offs across four primary axes: Server Compute Cost, Time to First Byte (TTFB), Data Freshness, and Search Engine Optimization (SEO).

text
+-------------------------------------------------------------------------------------------------+
|                                 THE RENDERING SPECTRUM TRADEOFF MATRIX                          |
+-------------------+-----------------------+-----------------------+-----------------------------+
| Strategy          | When is HTML Built?   | TTFB Latency          | Data Freshness              |
+-------------------+-----------------------+-----------------------+-----------------------------+
| Client-Side (CSR) | In Browser at runtime | Ultra-Fast (~20-50ms) | Live (Fetched via client)   |
|                   | via JavaScript        | (Static empty shell)  | Delayed by client waterfalls|
+-------------------+-----------------------+-----------------------+-----------------------------+
| Static (SSG)      | At Build Time         | Ultra-Fast (~10-30ms) | Static (Stale until rebuild)|
|                   | (npm run build)       | (Cached at Edge CDN)  |                             |
+-------------------+-----------------------+-----------------------+-----------------------------+
| Incremental (ISR) | At Build Time +       | Ultra-Fast (~10-30ms) | Semi-Fresh (Stale-While-    |
|                   | Background at runtime | (Edge CDN cached)     | Revalidate window)          |
+-------------------+-----------------------+-----------------------+-----------------------------+
| Dynamic (SSR)     | On Every Request      | Moderate to High      | 100% Real-Time Live Data    |
|                   | on the Origin Server  | (~150-800ms)          |                             |
+-------------------+-----------------------+-----------------------+-----------------------------+

1. Client-Side Rendering (CSR)#

In pure CSR, the server returns an empty HTML payload:

html
<!DOCTYPE html>
<html>
  <head><script src="/bundle.js" defer></script></head>
  <body><div id="root"></div></body>
</html>
  • The Waterfall Problem: The browser must download the HTML, download the JavaScript bundle (often 500 KB to 2 MB), parse and compile the JS, execute React, and only then dispatch an asynchronous fetch() request to an API server. Content is invisible to users and web crawlers until the entire chain completes.

2. Static Site Generation (SSG)#

In SSG, Next.js compiles the entire page, fetches its data, and renders full HTML and Flight data during the compilation build phase (npm run build).

  • The resulting .html and .json artifacts are deployed to globally distributed Edge Content Delivery Networks (CDNs) like Cloudflare or Vercel Edge Network.
  • Performance: Requests hit the edge cache closest to the user, resulting in sub-30 millisecond TTFB. However, updating content requires a full deployment build.

3. Incremental Static Regeneration (ISR)#

ISR bridges the gap between static speed and dynamic freshness:

  • Pages are generated statically at build time.
  • Each page specifies a revalidation interval: export const revalidate = 60; (in seconds).
  • The CDN serves the cached static page immediately to all incoming users.
  • When a request arrives after the 60-second window has expired, Next.js serves the stale cached page (zero wait for the user) while triggering an asynchronous background regeneration on the origin server.
  • Once the background build succeeds, the edge cache is updated atomically. Subsequent visitors receive the updated page.

4. Dynamic Server-Side Rendering (SSR)#

In dynamic SSR, the origin server executes the component tree, performs database queries, renders full HTML, and transmits the response for every individual incoming HTTP request.

  • Essential for authenticated dashboards, personalized content, and real-time inventory checks.
  • Traditional SSR suffers from the "All-or-Nothing" Bottleneck: the browser cannot display a single pixel of HTML until the slowest database query on the server completes.

2. Streaming SSR with React Suspense & Selective Hydration#

Modern Next.js eliminates the SSR all-or-nothing bottleneck by combining React Suspense with HTTP Chunked Transfer Encoding.

text
+-------------------------------------------------------------------------------------------------+
|                                 STREAMING SSR ARCHITECTURAL TIMELINE                            |
|                                                                                                 |
|   HTTP Header Sent Immediately: HTTP/1.1 200 OK, Transfer-Encoding: chunked                     |
|                                                                                                 |
|   Timestamp t = 20 ms: Chunk 1 (Instant Page Shell & Header)                                    |
|   <header>DevThesis</header><main><div id="skeleton">Loading Details...</div></main>            |
|   --> Browser paints Header and Skeleton instantly! FCP = 20 ms!                                |
|                                                                                                 |
|   Timestamp t = 80 ms: Chunk 2 (Product Info Completes)                                         |
|   <div id="product-info">Enterprise Plan: USD 99</div>                                          |
|                                                                                                 |
|   Timestamp t = 500 ms: Chunk 3 (Slow Database Reviews Query Completes)                         |
|   <template id="B:1"><div>User Review: 5 Stars</div></template>                                 |
|   <script>RC("skeleton", "B:1")</script>                                                       |
|   --> Tiny inline JS script swaps template directly into skeleton in live DOM! LCP Complete!    |
+-------------------------------------------------------------------------------------------------+

How Out-of-Order Streaming Works#

  1. Initial Shell Flush: The server renders all components outside of <Suspense> boundaries. It immediately flushes an initial HTML chunk containing the navigation bar, layout, and fallback skeletons:

```html

<div id="reviews-placeholder">

<div class="animate-pulse skeleton">Loading Reviews...</div>

</div>

```

  1. Background Server Execution: The slow component inside <Suspense> continues running in the background on the server.
  2. Chunk Replacement Script: When the slow component completes, the server writes a new chunk into the active HTTP stream containing:

- The rendered HTML encapsulated inside a hidden <template id="B:1"> tag.

- An inline JavaScript function call: RC("reviews-placeholder", "B:1").

  1. Instant In-Place Swap: The browser executes RC(), extracting the DOM nodes from the template and swapping them directly into the placeholder without requiring any client-side bundle re-rendering.

Selective Hydration#

In traditional SSR, hydration is monolithic: the browser must download the entire client JavaScript bundle and hydrate the entire page before any button becomes clickable.

With Selective Hydration:

  • React begins hydrating interactive components as soon as their code arrives, even if other parts of the page are still streaming HTML.
  • Interaction-Driven Priority: If the user clicks on an unhydrated component (e.g., clicking an "Add to Cart" button while the comments section is still streaming), React intercepts the click event, temporarily pauses background hydration, prioritizes hydrating the clicked button immediately, and synchronously replays the click event.

3. Dynamic Rendering Triggers & Cache Invalidation#

Next.js 15 automatically determines whether a route is Static (prerendered at build time) or Dynamic (rendered per request on the origin server).

text
+-------------------------------------------------------------------------------------------------+
|                                 DYNAMIC RENDERING DECISION MATRIX                               |
|                                                                                                 |
|   Does the Route Segment invoke any of the following DYNAMIC FUNCTIONS?                        |
|   - cookies()                                                                                   |
|   - headers()                                                                                   |
|   - searchParams (in page.tsx)                                                                  |
|   - un-cached fetch() requests                                                                  |
|                                 /                  \                                            |
|                               YES                  NO                                           |
|                               /                      \                                          |
|                     [DYNAMIC SSR]                 [STATIC SSG / ISR]                            |
|                     Rendered per request          Prerendered at build time;                    |
|                     on origin server              served from global Edge CDN                   |
+-------------------------------------------------------------------------------------------------+

Route Segment Configuration#

Developers can override automatic detection using explicit route segment flags:

typescript
// Force static generation; build fails if dynamic functions are invoked
export const dynamic = 'force-static';

// Force dynamic SSR on every request
export const dynamic = 'force-dynamic';

// Set ISR revalidation window to 300 seconds (5 minutes)
export const revalidate = 300;

On-Demand Cache Invalidation: `revalidatePath` and `revalidateTag`#

In addition to time-based ISR, Next.js provides On-Demand Cache Purging via Server Actions or API Route Handlers:

  1. Path-Based Revalidation (`revalidatePath`):

Purges the cached static HTML and RSC payload for a specific URL pathname:

```typescript

revalidatePath('/products/[slug]', 'page');

```

  1. Tag-Based Revalidation (`revalidateTag`):

Tagging allows purging hundreds of disparate pages with a single call:

```typescript

// In Server Component:

fetch('https://api.internal/products', { next: { tags: ['products'] } });

// In Admin Mutation Server Action:

revalidateTag('products'); // Purges all pages consuming this tag instantly!

```


4. Production Engineering Case Study: High-Traffic E-Commerce Product Detail Page (PDP)#

Consider an e-commerce platform with 2,000,000 product detail pages handling 50,000 requests per second during Black Friday sales.

text
+-------------------------------------------------------------------------------------------------+
|                                 HIGH-TRAFFIC PDP CACHING TOPOLOGY                               |
|                                                                                                 |
|   [Incoming Request: /products/macbook-pro]                                                     |
|                         |                                                                       |
|                         v                                                                       |
|   [Global Edge CDN (Cloudflare / Fastly)]                                                       |
|   - Header: Cache-Control: s-maxage=300, stale-while-revalidate=86400                           |
|                         |                                                                       |
|           Is Cached Static Shell Fresh (< 300s)?                                                |
|                 /                    \                                                          |
|               YES                    NO                                                         |
|               /                        \                                                        |
|   [Serve from Edge in 12ms]    Is Within SWR Window (< 86,400s)?                                |
|                                       /                  \                                      |
|                                     YES                  NO (Cold Cache)                        |
|                                     /                      \                                    |
|                   [Serve Stale Copy in 12ms]     [Origin SSR Generation]                        |
|                   [Trigger Async Revalidation]   - Stream Shell via Suspense                    |
|                   [Regenerate in Background]     - Fetch Live Stock via DB                      |
+-------------------------------------------------------------------------------------------------+

Edge Caching Headers Architecture#

To ensure the origin database is never overwhelmed by traffic spikes, the edge CDN is configured with stale-while-revalidate:

Cache-Control: public, s-maxage=300, stale-while-revalidate=86400

  • `s-maxage=300`: Edge CDN caches the rendered response for 300 seconds (5 minutes). Any request arriving within 5 minutes receives a cache hit directly from CDN edge memory.
  • `stale-while-revalidate=86400`: If a request arrives between minute 5 and hour 24, the CDN serves the cached copy immediately (zero latency for the user) while asynchronously dispatching a single background request to the Next.js origin server to revalidate the cache.
  • Thundering Herd Protection: The edge CDN coalesces concurrent requests during background revalidation, ensuring that only one request hits the origin database, preventing database collapse.

5. Anti-Patterns & Common Pitfalls#

The "Accidental Dynamic Rendering" Trap#

A catastrophic performance bug occurs when a developer reads headers() or cookies() in the root app/layout.tsx:

typescript
// CATASTROPHIC ANTI-PATTERN in app/layout.tsx
import { cookies } from "next/headers";

export default function RootLayout({ children }) {
  const cookieStore = cookies(); // DYNAMIC FUNCTION!
  const theme = cookieStore.get("theme")?.value || "dark";
  
  // INADVERTENTLY DE-OPTIMIZES EVERY SINGLE PAGE IN THE APP INTO DYNAMIC SSR!
  return <html className={theme}><body>{children}</body></html>;
}

Because the root layout wraps every route, invoking a dynamic function at the root permanently disables static SSG and ISR across the entire application, forcing every page to execute dynamic SSR on the origin server.

Remediation: Read theme cookies in a leaf client component or use inline script injection for theme toggling.

The Hydration Mismatch Error#

A hydration mismatch occurs when the server-rendered HTML string diverges from the initial virtual DOM tree computed by React on the client:

typescript
// ANTI-PATTERN: Non-deterministic rendering
export function LiveClock() {
  // Server renders timestamp at build time or server time;
  // Client renders timestamp at browser local time!
  return <div>{new Date().toLocaleTimeString()}</div>;
}

During hydration, React compares the server DOM text node with the client virtual DOM text node. Because the timestamps differ, React logs the infamous error:

Error: Text content does not match server-rendered HTML.

React is forced to discard the server-rendered DOM tree and re-render the element on the client, degrading performance and causing visual flashing.

Remediation: Two-Pass Rendering or `suppressHydrationWarning`:

typescript
"use client";
import { useState, useEffect } from "react";

export function LiveClock() {
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);

  if (!mounted) {
    return <div>Loading clock...</div>; // Matches server output exactly!
  }
  return <div>{new Date().toLocaleTimeString()}</div>;
}

6. Checkpoint Practice Problems#

The following four multi-part quantitative problems evaluate Core Web Vitals latency modeling across rendering topologies, ISR state machine timing, streaming chunked transfer delivery timelines, and hydration mismatch resolution. Complete step-by-step solutions follow each problem prompt.


Problem 1: Web Vitals Latency Modeling (TTFB, FCP, LCP, TTI) Across Rendering Topologies#

Context:

A performance engineering director is evaluating the Core Web Vitals metrics of an enterprise news publication portal across three rendering architectures:

  • Architecture 1: Pure Client-Side Rendering (CSR)
  • Architecture 2: Traditional Monolithic Server-Side Rendering (SSR)
  • Architecture 3: Incremental Static Regeneration (ISR / Edge SSG)

User Network & Hardware Profile (Mobile 4G Connection):

  • Network Round-Trip Time (RTT): RTT = 50 ms.
  • TLS Handshake & DNS latency: τ_{conn} = 100 ms.
  • Downlink bandwidth: B = 5 MB/sec = 5,120 KB/sec.
  • Mobile CPU processing capability: v_{parse} = 1 ms per 10 KB of JS parsed/compiled/executed.

Application Assets & Latency Parameters:

  • HTML Shell size: S_{html} = 20 KB.
  • Client JavaScript bundle size:

- CSR: S_{js_csr} = 800 KB.

- SSR: S_{js_ssr} = 250 KB (Hydration bundle).

- ISR: S_{js_isr} = 80 KB (Lightweight client component bundle).

  • Origin server database data fetching latency: T_{db} = 250 ms.
  • Origin server HTML rendering compute time: T_{render} = 50 ms.
  • Edge CDN cache hit latency: T_{edge} = 15 ms.

Formulas:

  • TTFB = τ_{conn} + RTT + T_{server_response}
  • DownloadTime(S) = S / B
  • FCP = TTFB + DownloadTime(S_{html}) + DOM Parse Time (10 ms) + Render Initial Visible Content
  • LCP = Timestamp when largest text/hero element is fully painted
  • TTI = Timestamp when JS is fully downloaded, parsed, and hydrated

Questions:

  1. Calculate the exact Time to First Byte (TTFB) for all three architectures.
  2. Calculate the First Contentful Paint (FCP) for all three architectures.
  3. Calculate the Largest Contentful Paint (LCP) for all three architectures.
  4. Calculate the Time to Interactive (TTI) for all three architectures.
  5. Summarize the findings: which architecture achieves Google Core Web Vitals "Good" thresholds (LCP ≤ 2.5 s, TTFB ≤ 0.8 s)?

Solution:

Step 1: Calculate Time to First Byte (TTFB)

  • Architecture 1 (CSR):

Server returns static empty HTML immediately from static host (T_{server} = 10 ms):

TTFB_{CSR} = τ_{conn} + RTT + T_{server} = 100 + 50 + 10 = 160.0 ms.

  • Architecture 2 (Traditional SSR):

Server must execute DB query (250 ms) and render HTML (50 ms) before sending byte 1:

T_{server_ssr} = 250 + 50 = 300 ms.

TTFB_{SSR} = τ_{conn} + RTT + T_{server_ssr} = 100 + 50 + 300 = 450.0 ms.

  • Architecture 3 (ISR / Edge Cache Hit):

Edge CDN serves cached static artifact in T_{edge} = 15 ms:

TTFB_{ISR} = τ_{conn} + RTT + T_{edge} = 100 + 50 + 15 = 165.0 ms.

Step 2: Calculate First Contentful Paint (FCP)

  • HTML download time: 20 KB / 5,120 KB/s ≈ 3.9 ms.
  • DOM initial parse = 10.0 ms.
  • Architecture 1 (CSR):

Empty shell paints nothing meaningful. FCP is delayed until the 800 KB JS bundle downloads, executes, and calls API:

JS download = 800 / 5,120 ≈ 156.25 ms.

JS compile/execute = (800 / 10) × 1 ms = 80.0 ms.

API request: RTT + T_{db} = 50 + 250 = 300.0 ms.

FCP_{CSR} = TTFB (160) + 3.9 + 10 + 156.25 + 80.0 + 300.0 = 710.15 ms.

  • Architecture 2 (Traditional SSR):

HTML contains full pre-rendered text. FCP occurs immediately upon HTML parse:

FCP_{SSR} = TTFB (450) + 3.9 + 10.0 = 463.9 ms.

  • Architecture 3 (ISR):

HTML contains full pre-rendered text from Edge CDN:

FCP_{ISR} = TTFB (165) + 3.9 + 10.0 = 178.9 ms.

Step 3: Calculate Largest Contentful Paint (LCP)

  • For SSR and ISR, the largest article hero text is already present in the pre-rendered HTML payload. Therefore, LCP ≈ FCP:

LCP_{SSR} = 463.9 ms.

LCP_{ISR} = 178.9 ms.

  • For CSR, the hero article appears only after the API data returns and renders:

LCP_{CSR} = 710.15 ms.

Step 4: Calculate Time to Interactive (TTI)

TTI occurs when JS is downloaded, parsed, and the page is hydrated and responsive to user input:

  • Architecture 1 (CSR):

TTI_{CSR} = LCP_{CSR} = 710.15 ms.

  • Architecture 2 (SSR):

Must download 250 KB hydration bundle:

Download = 250 / 5,120 = 48.83 ms.

Parse/hydrate = (250 / 10) × 1 = 25.0 ms.

TTI_{SSR} = FCP (463.9) + 48.83 + 25.0 = 537.73 ms.

  • Architecture 3 (ISR):

Must download only 80 KB bundle:

Download = 80 / 5,120 = 15.63 ms.

Parse/hydrate = (80 / 10) × 1 = 8.0 ms.

TTI_{ISR} = FCP (178.9) + 15.63 + 8.0 = 202.53 ms.

Step 5: Core Web Vitals Summary

  • ISR: TTFB = 165 ms, FCP = 179 ms, LCP = 179 ms, TTI = 203 ms (Exceptional / Elite tier).
  • SSR: TTFB = 450 ms, FCP = 464 ms, LCP = 464 ms, TTI = 538 ms (Good tier).
  • CSR: TTFB = 160 ms, FCP = 710 ms, LCP = 710 ms, TTI = 710 ms (Acceptable, but 4x slower LCP than ISR).

Problem 2: Incremental Static Regeneration (ISR) Stale-While-Revalidate State Machine#

Context:

An author publishes an article on a Next.js 15 blog configured with an ISR revalidation interval:

export const revalidate = 60; // 60 seconds

Operational Parameters:

  • Initial build completes at timestamp t = 0 s (Version V_1).
  • Background regeneration duration on origin server: T_{regen} = 4.0 seconds.
  • Content author publishes an update (Version V_2) to the CMS at timestamp t = 45 s.

Incoming Request Timeline:

  • Request 1: t = 10 s
  • Request 2: t = 50 s
  • Request 3: t = 65 s
  • Request 4: t = 67 s
  • Request 5: t = 70 s
  • Request 6: t = 135 s

Questions:

  1. For each of the 6 incoming requests, determine:

- Whether the request experiences a Cache Hit (Fresh), Stale Cache Hit, or Cache Miss.

- Which version of content (V_1 or V_2) is returned to the user.

- The latency experienced by the user (15 ms CDN hit vs 4,000 ms origin generation).

- Whether an asynchronous background regeneration is triggered.

  1. Exactly at what timestamp does Version V_2 become visible in the CDN cache?
  2. What would occur if 50 concurrent users requested the page simultaneously at t = 65 s? How does Next.js prevent a thundering herd on the database?

Solution:

Step 1: Request Timeline Analysis

  • Request 1 (t = 10 s):

- Elapsed since last generation = 10 s < 60 s. Cache is Fresh.

- Version returned: V_1.

- Latency: 15 ms (CDN Hit).

- Background regeneration: No.

  • Request 2 (t = 50 s):

- Elapsed = 50 s < 60 s. Cache is Fresh (CMS update at t = 45 s is not seen yet).

- Version returned: V_1.

- Latency: 15 ms (CDN Hit).

- Background regeneration: No.

  • Request 3 (t = 65 s):

- Elapsed = 65 s > 60 s. Window expired!

- Cache status: Stale-While-Revalidate Hit.

- Version returned: V_1 (Stale copy served instantly; zero wait for user).

- Latency: 15 ms (CDN Hit).

- Background regeneration: YES. Triggers background rebuild of page at t = 65 s.

  • Request 4 (t = 67 s):

- Background regeneration is currently in progress (65 s → 69 s).

- Cache status: Stale Hit.

- Version returned: V_1.

- Latency: 15 ms (CDN Hit).

- Background regeneration: No (An active build is already locked and running).

  • Request 5 (t = 70 s):

- Background regeneration finished at t = 65 + 4 = 69.0 s, publishing V_2.

- Elapsed since new generation (t = 69 s) = 70 - 69 = 1 s < 60 s. Cache is Fresh.

- Version returned: V_2!

- Latency: 15 ms (CDN Hit).

- Background regeneration: No.

  • Request 6 (t = 135 s):

- Elapsed since last build (t = 69 s) = 135 - 69 = 66 s > 60 s. Window expired!

- Cache status: Stale Hit.

- Version returned: V_2.

- Latency: 15 ms (CDN Hit).

- Background regeneration: YES (Triggers rebuild to check for new updates).

Step 2: Timestamp of V_2 Activation

Background regeneration initiated at t = 65.0 s, takes 4.0 s.

V_2 becomes active in CDN cache at timestamp t = 69.0 seconds.

Step 3: Thundering Herd Mitigation

If 50 concurrent requests hit the edge at t = 65 s:

  • Next.js and the Edge CDN employ an in-flight regeneration mutex lock.
  • All 50 users immediately receive the cached V_1 page in 15 ms.
  • Exactly one single background task is dispatched to the origin server to generate the new page. The origin database receives exactly 1 query, completely eliminating database stampedes.

Problem 3: Streaming SSR Chunked Transfer Encoding & Selective Hydration Timing#

Context:

An analytics dashboard page is architected with three nested components using React Suspense:

typescript
// app/dashboard/page.tsx
export default function Dashboard() {
  return (
    <div>
      <Header />
      <Suspense fallback={<MetricsSkeleton />}>
        <RealtimeMetrics />
      </Suspense>
      <Suspense fallback={<LedgerSkeleton />}>
        <HistoricalLedger />
      </Suspense>
    </div>
  );
}

Component Performance Profiles on Server:

  • <Header />: Synchronous server component. Compute time: t_{header} = 10 ms.
  • <RealtimeMetrics />: Async component (Redis cache lookup). Compute time: t_{metrics} = 80 ms.
  • <HistoricalLedger />: Async component (Deep PostgreSQL aggregation). Compute time: t_{ledger} = 500 ms.

Network Parameters:

  • Network RTT between client and server: 40 ms.
  • Streaming transmission overhead per chunk: 5 ms.

Questions:

  1. Identify the exact server timestamps (t = 0 is request arrival) when Chunk 1, Chunk 2, and Chunk 3 are flushed into the HTTP response stream.
  2. Outline the contents of Chunk 1, Chunk 2, and Chunk 3.
  3. Calculate the client-perceived First Contentful Paint (FCP) and Largest Contentful Paint (LCP).
  4. If the user clicks a tab inside <RealtimeMetrics /> at timestamp t = 200 ms (while <HistoricalLedger /> is still computing on the server), explain how React Selective Hydration handles the click.

Solution:

Step 1: Chunk Flush Timestamps

  • Chunk 1 Flush: Immediately when synchronous shell completes.

Timestamp: t_1 = t_{header} = 10 ms.

  • Chunk 2 Flush: When <RealtimeMetrics /> completes.

Timestamp: t_2 = t_{metrics} = 80 ms.

  • Chunk 3 Flush: When <HistoricalLedger /> completes.

Timestamp: t_3 = t_{ledger} = 500 ms.

Step 2: Contents of Each Streamed Chunk

  • Chunk 1 (t = 10 ms):

HTML header, DOCTYPE, navigation markup, and fallback skeletons:

<header>Dashboard</header><div id="P:metrics"><MetricsSkeleton/></div><div id="P:ledger"><LedgerSkeleton/></div>

  • Chunk 2 (t = 80 ms):

Rendered HTML for metrics wrapped in a template and the replacement script:

<template id="T:metrics"><div>Live Metrics: 1.2M</div></template><script>RC("P:metrics", "T:metrics")</script>

  • Chunk 3 (t = 500 ms):

Rendered HTML for ledger and final replacement script:

<template id="T:ledger"><table>...ledger rows...</table></template><script>RC("P:ledger", "T:ledger")</script>

Step 3: Client-Perceived FCP and LCP Milestones

  • FCP: When Chunk 1 arrives in browser:

FCP = Server Time (10 ms) + Network (40 ms) + Parse (5 ms) = 55.0 ms.

The user sees the header and skeletons in 55 ms!

  • LCP: The largest content element is the data table in <HistoricalLedger /> arriving in Chunk 3:

LCP = Server Time (500 ms) + Network (40 ms) + Parse (5 ms) = 545.0 ms.

Step 4: Selective Hydration Behavior at t = 200 ms

At t = 200 ms:

  • Chunk 2 arrived at t = 125 ms. <RealtimeMetrics /> is already rendered in the DOM.
  • Chunk 3 has not arrived yet (still computing on server until 500 ms).
  • When the user clicks the tab at t = 200 ms, React intercepts the native DOM event.
  • React checks the Fiber tree: <RealtimeMetrics /> code is already loaded, but not yet hydrated.
  • React immediately initiates Selective Hydration on <RealtimeMetrics />, hydrates its hook linked list in ~2 ms, and synchronously dispatches the queued click event.
  • The tab switches instantly at t = 202 ms, completely unhindered by the fact that the ledger table has not even finished querying on the server!

Problem 4: Hydration Mismatch Diagnosis & Deterministic SSR Stabilization#

Context:

A senior frontend engineer is auditing a user profile header component in Next.js 15:

typescript
// app/components/UserGreeting.tsx
export function UserGreeting({ username }: { username: string }) {
  const isClientMobile = typeof window !== "undefined" && window.innerWidth < 768;
  const loginTimestamp = new Date().toLocaleDateString("en-US");

  return (
    <div className={isClientMobile ? "mobile-header" : "desktop-header"}>
      <h1>Welcome back, {username}!</h1>
      <p>Session started: {loginTimestamp}</p>
    </div>
  );
}

When loaded in production, Chrome DevTools logs two distinct Hydration Mismatch warnings and the UI visibly flashes:

  1. Warning: Prop 'className' did not match. Server: "desktop-header" Client: "mobile-header"
  2. Warning: Text content did not match server-rendered HTML.

Questions:

  1. Identify the exact root cause of both hydration mismatches.
  2. Explain the performance penalty imposed on the browser when React encounters a hydration mismatch.
  3. Refactor UserGreeting using CSS media queries and a deterministic client-mounting hook (useIsMounted) to eliminate both mismatches completely.
  4. Show how suppressHydrationWarning can be applied specifically to date text nodes when non-deterministic date formatting is unavoidable.

Solution:

Step 1: Root Cause Analysis

  • Mismatch 1 (`className`):

During server rendering, window is undefined. isClientMobile evaluates to false, generating className="desktop-header".

On the client mobile device, window.innerWidth is 390 < 768. isClientMobile evaluates to true, generating className="mobile-header". The virtual DOM node attribute does not match the server HTML attribute.

  • Mismatch 2 (`loginTimestamp`):

new Date() executed on the server uses the server's local clock and server timezone. When executed in the browser, it uses the user's local clock. If the server is in UTC and the client is in PST, the date string diverges.

Step 2: Browser Performance Penalty

When a mismatch occurs:

  1. React logs a diagnostic error.
  2. React abandons hydration for that subtree.
  3. React destroys the pre-rendered HTML DOM nodes and issues expensive native DOM operations (document.createElement, replaceChild) to re-render the client's virtual DOM tree from scratch.
  4. This causes visual layout shifting (CLS penalty) and locks the browser main thread during hydration.

Step 3: Refactored Deterministic Component

To solve Mismatch 1: Replace JavaScript window width checks with CSS media queries (which execute natively in the browser layout engine without React state).

To solve Mismatch 2: Defer non-deterministic client-only state until after mount:

typescript
"use client";
import { useState, useEffect } from "react";

export function UserGreeting({ username }: { username: string }) {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    // Runs exclusively on the client AFTER initial hydration completes!
    setMounted(true);
  }, []);

  return (
    // Use responsive CSS utility classes instead of JS window conditionals!
    <div className="header-base md:desktop-header max-md:mobile-header">
      <h1>Welcome back, {username}!</h1>
      <p>
        Session started:{" "}
        {mounted ? (
          new Date().toLocaleDateString("en-US")
        ) : (
          <span className="opacity-0">Loading date...</span>
        )}
      </p>
    </div>
  );
}

Step 4: Alternative with `suppressHydrationWarning`

If the developer wishes to render the timestamp directly without an empty loading state:

typescript
<p suppressHydrationWarning>
  Session started: {new Date().toLocaleDateString("en-US")}
</p>

suppressHydrationWarning instructs React's reconciler to ignore attribute or text differences on that specific element up to one level deep, preventing tree destruction and console error spam.

Key Architectural Takeaways

  • ISR combines static CDN speed with dynamic data freshness using background stale-while-revalidate revalidation.
  • Hydration mismatches occur when server-rendered HTML diverges from client initial render output.
  • React Server Components stream flight payloads, enabling fast HTML rendering while slow components resolve in Suspense.
  • Edge Middleware executes lightweight V8 logic at the CDN edge for sub-10ms redirects and A/B testing.

Hands-on Code Playground Available

Execute, edit, and experiment with code for this topic in the live sandbox.

Practice Check • Quick Quiz

Test Your Understanding

In Incremental Static Regeneration (ISR), what response does a user receive when requesting a page whose revalidate time window has already expired?

A
An HTTP 504 Gateway Timeout error while the server rebuilds the page
B
The stale cached HTML page immediately, while Next.js triggers background page regeneration asynchronously
C
A blank white screen until the database query completes
D
A redirect to the homepage