Back to Blog

The Secret Life of a Next.js Request

A complete architectural walkthrough of what happens from browser request to response — middleware, edge runtime, RSC payload, streaming, caching layers, and where your code actually runs.


The Illusion of Simplicity

You write a page component. You deploy to Vercel. Users visit your site. It works.

But between the browser's fetch and your component's render, there's a complex orchestration of runtimes, caches, serialization protocols, and streaming mechanisms. Understanding this architecture isn't academic — it's the difference between a 200ms page load and a 2000ms one.

┌─────────────────────────────────────────────────────────────────────────────┐
│                    THE 30,000-FOOT VIEW                                      │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Browser                                                                     │
│     │                                                                        │
│     ▼                                                                        │
│  CDN Edge (cache check) ────────────────────────────────┐                   │
│     │                                                    │ HIT: return      │
│     │ MISS                                               │ cached response  │
│     ▼                                                    │                   │
│  Middleware (Edge Runtime) ─────────────────────────────┼──▶ redirect/     │
│     │                                                    │    rewrite       │
│     │ continue                                           │                   │
│     ▼                                                    │                   │
│  Route Handler / Page                                    │                   │
│     │                                                    │                   │
│     ├── Static? ───────▶ Serve from CDN                 │                   │
│     │                                                    │                   │
│     ├── ISR? ──────────▶ Check stale, maybe regenerate  │                   │
│     │                                                    │                   │
│     └── Dynamic? ──────▶ Server render (Node.js)        │                   │
│            │                                             │                   │
│            ▼                                             │                   │
│       RSC Payload Generation                             │                   │
│            │                                             │                   │
│            ▼                                             │                   │
│       Stream HTML + RSC Payload ◀────────────────────────┘                   │
│            │                                                                 │
│            ▼                                                                 │
│       Browser: Parse, Hydrate, Interactive                                  │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Let's trace a request through every layer.


Phase 1: The Browser Makes a Request

Initial Navigation

// User clicks a link or types URL
// Browser initiates navigation

GET /dashboard HTTP/2
Host: app.example.com
Accept: text/html,application/xhtml+xml,...
Accept-Encoding: gzip, br
Cookie: session=abc123

For initial page loads, the browser requests HTML. This is the full page load path.

Client-Side Navigation (Soft Navigation)

// User clicks <Link> component
// Next.js Router intercepts

// Instead of full HTML, browser requests RSC payload
GET /dashboard HTTP/2
Host: app.example.com
Accept: text/x-component          // RSC payload format
RSC: 1                             // Signal: this is RSC request
Next-Router-State-Tree: [encoded] // Current router state
Next-Router-Prefetch: 1           // If prefetching

The RSC: 1 header tells the server: "Don't send HTML, send the React Server Component payload."

┌─────────────────────────────────────────────────────────────────────────────┐
│                    TWO REQUEST TYPES                                         │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  INITIAL LOAD (hard navigation):                                            │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │  Request: GET /dashboard (Accept: text/html)                        │    │
│  │  Response: Full HTML document                                       │    │
│  │    <!DOCTYPE html>                                                  │    │
│  │    <html>                                                           │    │
│  │      <head>...</head>                                               │    │
│  │      <body>                                                         │    │
│  │        <div id="__next">...rendered HTML...</div>                   │    │
│  │        <script>...RSC payload embedded...</script>                  │    │
│  │        <script src="/_next/static/chunks/..."/>                     │    │
│  │      </body>                                                        │    │
│  │    </html>                                                          │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
│  CLIENT NAVIGATION (soft navigation):                                       │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │  Request: GET /dashboard (RSC: 1)                                   │    │
│  │  Response: RSC payload only (streamed)                              │    │
│  │    0:["$","div",null,{"children":...}]                              │    │
│  │    1:["$","$L2",null,{"data":...}]                                  │    │
│  │    2:I["./components/Chart.js",...]                                 │    │
│  │    ...                                                              │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Phase 2: CDN Edge Layer

The request first hits the CDN edge (Cloudflare, Vercel Edge Network, AWS CloudFront).

Cache Lookup

┌─────────────────────────────────────────────────────────────────────────────┐
│                    CDN CACHE DECISION TREE                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Request arrives at edge                                                     │
│       │                                                                      │
│       ▼                                                                      │
│  Is route static? (pre-rendered at build)                                   │
│       │                                                                      │
│       ├── Yes ──▶ Check CDN cache                                           │
│       │              │                                                       │
│       │              ├── HIT ──▶ Return cached HTML (< 10ms)                │
│       │              │                                                       │
│       │              └── MISS ──▶ Fetch from origin, cache, return          │
│       │                                                                      │
│       └── No (dynamic) ──▶ Pass to origin                                   │
│                              │                                               │
│                              ▼                                               │
│                         Is ISR route?                                        │
│                              │                                               │
│                              ├── Yes ──▶ Check stale-while-revalidate      │
│                              │              │                                │
│                              │              ├── Fresh ──▶ Return cached     │
│                              │              │                                │
│                              │              └── Stale ──▶ Return cached,    │
│                              │                           trigger background │
│                              │                           regeneration       │
│                              │                                               │
│                              └── No ──▶ Execute middleware + render         │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Cache Headers That Matter

// next.config.js controls default caching
// But individual routes can override

// Static page (default)
Cache-Control: public, max-age=31536000, immutable

// ISR page
Cache-Control: s-maxage=60, stale-while-revalidate

// Dynamic page (no cache)
Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate

// API route with revalidation
Cache-Control: s-maxage=3600, stale-while-revalidate=86400

Phase 3: Middleware Execution

If the request passes the CDN cache (miss or dynamic), middleware executes at the edge.

Where Middleware Runs

┌─────────────────────────────────────────────────────────────────────────────┐
│                    MIDDLEWARE EXECUTION CONTEXT                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  EDGE RUNTIME (V8 Isolates):                                                │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │  • Runs in Cloudflare Workers / Vercel Edge Functions               │    │
│  │  • Globally distributed (runs near user)                            │    │
│  │  • Cold start: ~0ms (V8 isolates, not containers)                   │    │
│  │  • Max execution: 30 seconds (varies by platform)                   │    │
│  │  • No Node.js APIs (no fs, no child_process)                        │    │
│  │  • Limited to Web APIs (fetch, crypto, TextEncoder, etc.)           │    │
│  │  • Can't import Node.js packages                                    │    │
│  │  • Memory: ~128MB                                                   │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
│  Available APIs:                                                             │
│  ├── Request / Response (standard Web API)                                  │
│  ├── fetch() (native, not node-fetch)                                       │
│  ├── crypto.subtle (WebCrypto)                                              │
│  ├── TextEncoder / TextDecoder                                              │
│  ├── URL / URLSearchParams                                                   │
│  ├── Headers                                                                 │
│  ├── cookies() — Next.js helper                                             │
│  ├── headers() — Next.js helper                                             │
│  └── NextResponse — redirect, rewrite, json, next()                         │
│                                                                              │
│  NOT Available:                                                              │
│  ├── fs (filesystem)                                                        │
│  ├── path                                                                   │
│  ├── child_process                                                          │
│  ├── Most npm packages (if they use Node.js APIs)                          │
│  └── Long-running connections (WebSockets terminate differently)            │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Middleware Execution Flow

// middleware.ts (runs on EVERY request by default)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // 1. Request arrives — you have full access to:
  const url = request.nextUrl;           // Parsed URL
  const cookies = request.cookies;        // Cookie jar
  const headers = request.headers;        // Request headers
  const geo = request.geo;               // Geolocation (Vercel)
  const ip = request.ip;                 // Client IP (Vercel)

  // 2. Make decisions
  const session = cookies.get('session');

  if (!session && url.pathname.startsWith('/dashboard')) {
    // Redirect: returns 307, browser makes new request
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // 3. Rewrite (internal routing change)
  if (url.pathname === '/old-page') {
    // URL stays /old-page, but /new-page is rendered
    return NextResponse.rewrite(new URL('/new-page', request.url));
  }

  // 4. Modify request before it reaches route
  const response = NextResponse.next();

  // Add headers that route handlers will see
  response.headers.set('x-user-country', geo?.country ?? 'unknown');

  // 5. Modify response on the way back
  response.cookies.set('visited', 'true', { httpOnly: true });

  return response;
}

// CRITICAL: Configure which routes middleware runs on
export const config = {
  matcher: [
    // Match all except static files and api routes you want to skip
    '/((?!_next/static|_next/image|favicon.ico|api/health).*)',
  ],
};

Middleware Timing Impact

┌─────────────────────────────────────────────────────────────────────────────┐
│                    MIDDLEWARE LATENCY                                        │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Middleware adds latency to EVERY matched request.                          │
│                                                                              │
│  Operation                              Typical Latency                      │
│  ─────────────────────────────────────────────────────────────────────────  │
│  Cookie/header inspection               <1ms                                │
│  Simple redirect/rewrite                <1ms                                │
│  JWT verification (edge)                1-5ms                               │
│  KV/Redis lookup (same region)          5-20ms                              │
│  External API call                      50-500ms ⚠️                         │
│  Database query                         Not available (Node.js only)        │
│                                                                              │
│  Best practices:                                                             │
│  • Keep middleware fast (<10ms)                                             │
│  • Avoid external calls if possible                                         │
│  • Use edge-compatible auth (JWT, not session DB lookup)                    │
│  • Narrow matcher to only routes that need middleware                       │
│                                                                              │
│  If you need database access in auth:                                       │
│  → Don't do it in middleware                                                │
│  → Do it in a Server Component or API route (Node.js runtime)              │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Phase 4: Route Resolution

After middleware, Next.js resolves which handler should process the request.

Route Matching Order

┌─────────────────────────────────────────────────────────────────────────────┐
│                    ROUTE RESOLUTION PRIORITY                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Request: GET /dashboard/settings/profile                                   │
│                                                                              │
│  Next.js checks (in order):                                                 │
│                                                                              │
│  1. Static files: /public/dashboard/settings/profile (if exists)            │
│                                                                              │
│  2. Exact match: /app/dashboard/settings/profile/page.tsx                   │
│                                                                              │
│  3. Dynamic segments: /app/dashboard/settings/[section]/page.tsx            │
│     where section = "profile"                                               │
│                                                                              │
│  4. Catch-all: /app/dashboard/[...slug]/page.tsx                           │
│     where slug = ["settings", "profile"]                                    │
│                                                                              │
│  5. Optional catch-all: /app/dashboard/[[...slug]]/page.tsx                │
│     where slug = ["settings", "profile"]                                    │
│                                                                              │
│  6. Not found: Return 404                                                   │
│                                                                              │
│  Priority rules:                                                             │
│  • More specific routes win over less specific                              │
│  • Static segments beat dynamic segments                                    │
│  • Single dynamic beats catch-all                                           │
│  • Catch-all beats optional catch-all                                       │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Parallel and Intercepting Routes

┌─────────────────────────────────────────────────────────────────────────────┐
│                    ADVANCED ROUTING                                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  PARALLEL ROUTES (@folder):                                                  │
│  ─────────────────────────                                                  │
│  /app/dashboard/                                                             │
│    ├── layout.tsx                                                           │
│    ├── page.tsx                                                             │
│    ├── @analytics/page.tsx    ──┐                                           │
│    └── @notifications/page.tsx ─┴──▶ Both render in same layout            │
│                                                                              │
│  // layout.tsx                                                               │
│  export default function Layout({                                           │
│    children,                                                                │
│    analytics,        // @analytics slot                                     │
│    notifications,    // @notifications slot                                 │
│  }) {                                                                       │
│    return (                                                                 │
│      <div>                                                                  │
│        <main>{children}</main>                                              │
│        <aside>{analytics}</aside>                                           │
│        <aside>{notifications}</aside>                                       │
│      </div>                                                                 │
│    );                                                                       │
│  }                                                                          │
│                                                                              │
│  INTERCEPTING ROUTES ((..)folder):                                          │
│  ─────────────────────────────────                                          │
│  Soft navigation intercepts, hard navigation doesn't                       │
│                                                                              │
│  /app/                                                                       │
│    ├── photos/                                                              │
│    │   └── [id]/page.tsx         ──▶ Full photo page (hard nav)            │
│    └── @modal/                                                              │
│        └── (..)photos/[id]/page.tsx ──▶ Modal (soft nav)                   │
│                                                                              │
│  Click <Link href="/photos/123">                                            │
│    → Intercept: show modal                                                  │
│  Direct URL /photos/123                                                     │
│    → No intercept: show full page                                           │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Phase 5: Server-Side Rendering

The resolved route now executes. Here's where it gets interesting.

Runtime Selection

// Route Segment Config — choose your runtime

// app/dashboard/page.tsx
export const runtime = 'nodejs';  // Default — full Node.js
// OR
export const runtime = 'edge';    // Edge Runtime — limited but fast

// Other segment configs
export const dynamic = 'force-dynamic';  // Never cache
export const dynamic = 'force-static';   // Always static
export const dynamic = 'auto';           // Let Next.js decide (default)

export const revalidate = 60;            // ISR: revalidate every 60s
export const revalidate = false;         // Never revalidate (static)
export const revalidate = 0;             // Always revalidate (dynamic)

export const fetchCache = 'force-cache'; // Cache all fetches
export const fetchCache = 'force-no-store'; // Never cache fetches

The Render Tree

┌─────────────────────────────────────────────────────────────────────────────┐
│                    COMPONENT TREE EXECUTION                                  │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Request: /dashboard/settings                                               │
│                                                                              │
│  File structure:                                                             │
│  /app                                                                        │
│    ├── layout.tsx          ← Root Layout (Server Component)                │
│    └── dashboard                                                             │
│        ├── layout.tsx      ← Dashboard Layout (Server Component)            │
│        └── settings                                                          │
│            └── page.tsx    ← Settings Page (Server Component)               │
│                                                                              │
│  Render order (parallel where possible):                                    │
│                                                                              │
│  1. Root Layout renders                                                     │
│     ├── Can fetch data                                                      │
│     ├── Defines <html>, <body>                                              │
│     └── Renders {children} slot                                             │
│                                                                              │
│  2. Dashboard Layout renders (parallel with siblings)                       │
│     ├── Can fetch data                                                      │
│     ├── Defines dashboard chrome                                            │
│     └── Renders {children} slot                                             │
│                                                                              │
│  3. Settings Page renders                                                   │
│     ├── Can fetch data                                                      │
│     ├── Can use Suspense for streaming                                      │
│     └── Returns JSX                                                         │
│                                                                              │
│  IMPORTANT: Layouts DON'T re-render on navigation between child routes     │
│  /dashboard/settings → /dashboard/profile                                   │
│  Root Layout: preserved                                                      │
│  Dashboard Layout: preserved                                                 │
│  Page: re-rendered                                                          │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Server Component Execution

// app/dashboard/page.tsx — Server Component (default)

async function DashboardPage() {
  // This code runs ONLY on the server
  // Never sent to the browser
  // Can access server resources directly

  // 1. Direct database access (no API needed)
  const user = await db.users.findUnique({
    where: { id: getCurrentUserId() },
  });

  // 2. File system access
  const config = await fs.readFile('./config.json', 'utf-8');

  // 3. Environment variables (server-only)
  const apiKey = process.env.SECRET_API_KEY; // Safe — not exposed

  // 4. Fetch with automatic caching
  const data = await fetch('https://api.example.com/data', {
    // Next.js extends fetch with caching options
    next: { revalidate: 3600 }, // Cache for 1 hour
  });

  // 5. Return JSX — serialized to RSC payload
  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <Suspense fallback={<Loading />}>
        {/* This can stream in later */}
        <SlowComponent />
      </Suspense>
      {/* Client Component — marked with 'use client' */}
      <InteractiveChart data={data} />
    </div>
  );
}

What Can't Run on the Server

┌─────────────────────────────────────────────────────────────────────────────┐
│                    SERVER VS CLIENT CAPABILITIES                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  SERVER COMPONENTS CAN:              SERVER COMPONENTS CANNOT:              │
│  ─────────────────────               ───────────────────────                │
│  ✓ Access databases directly         ✗ Use useState, useEffect              │
│  ✓ Read files                         ✗ Use browser APIs (window, document) │
│  ✓ Use server-only packages          ✗ Add event handlers (onClick)        │
│  ✓ Keep secrets safe                 ✗ Use useContext (client context)     │
│  ✓ Reduce bundle size                ✗ Access refs                          │
│  ✓ Fetch with automatic caching      ✗ Use lifecycle hooks                  │
│  ✓ Stream with Suspense              ✗ Import CSS modules (client only)    │
│                                                                              │
│  CLIENT COMPONENTS CAN:              CLIENT COMPONENTS CANNOT:              │
│  ─────────────────────               ───────────────────────                │
│  ✓ Use all React hooks               ✗ Direct database/file access         │
│  ✓ Use browser APIs                  ✗ Use server-only packages            │
│  ✓ Add interactivity                 ✗ Keep secrets safe                   │
│  ✓ Use context providers             ✗ Import Server Components directly   │
│  ✓ Handle events                         (must pass as children)           │
│  ✓ Manage local state                                                      │
│                                                                              │
│  KEY: Client Components ARE rendered on the server first (SSR),             │
│       then hydrated on the client. They run in both places.                 │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Phase 6: RSC Payload Generation

Server Components don't send their JavaScript to the browser. Instead, they serialize to an RSC payload.

The RSC Wire Format

┌─────────────────────────────────────────────────────────────────────────────┐
│                    RSC PAYLOAD FORMAT                                        │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  The server renders components and produces a stream like:                  │
│                                                                              │
│  0:["$","div",null,{"className":"container","children":"$L1"}]              │
│  1:["$","main",null,{"children":["$","$L2",null,{"userId":123}]}]          │
│  2:I["client-component.js","ClientChart",null]                              │
│  3:["$","p",null,{"children":"Static text"}]                                │
│                                                                              │
│  Breaking it down:                                                           │
│  ─────────────────                                                          │
│                                                                              │
│  0: ─ Row ID (references)                                                    │
│  ["$","div",...] ─ Encoded React element                                    │
│    "$" ─ React element marker                                               │
│    "div" ─ Element type                                                     │
│    null ─ Key                                                               │
│    {...} ─ Props                                                            │
│                                                                              │
│  "$L1" ─ Lazy reference to row 1 (will stream later)                        │
│                                                                              │
│  "I" prefix ─ Client Component import instruction                           │
│    ["client-component.js","ClientChart",null]                               │
│    → Import ClientChart from client-component.js                            │
│    → This component's code IS sent to browser                               │
│                                                                              │
│  Server Component code is NOT in the payload ─ only their output            │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Serialization Rules

// What can be passed from Server to Client Components?

// ✓ Serializable primitives
const props = {
  string: "hello",
  number: 42,
  boolean: true,
  null: null,
  undefined: undefined,
  bigint: 100n,
  date: new Date(),         // Serialized specially
  array: [1, 2, 3],
  object: { nested: "ok" },
};

// ✓ React elements (including Server Components as children)
<ClientComponent>
  <ServerComponent />  {/* Rendered, serialized, passed as children */}
</ClientComponent>

// ✗ Functions (cannot be serialized)
<ClientComponent onClick={() => {}} />  // ERROR

// ✗ Classes, Symbols, other non-serializable
<ClientComponent data={new Map()} />    // ERROR

// ✗ Circular references
const obj = { self: null };
obj.self = obj;
<ClientComponent data={obj} />          // ERROR

// EXCEPTION: Server Actions (functions that run on server)
// These ARE allowed because they're actually RPC references
<ClientComponent action={serverAction} />  // OK — action runs on server

Phase 7: Streaming and Suspense

How Streaming Works

┌─────────────────────────────────────────────────────────────────────────────┐
│                    STREAMING TIMELINE                                        │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Without Streaming (traditional SSR):                                       │
│  ───────────────────────────────────                                        │
│  t=0      Server starts rendering                                           │
│  t=500ms  Waiting for slow database query...                                │
│  t=2000ms Slow query completes                                              │
│  t=2100ms Entire HTML sent to browser                                       │
│  t=2200ms Browser starts parsing                                            │
│                                                                              │
│  User sees NOTHING for 2+ seconds                                           │
│                                                                              │
│  ─────────────────────────────────────────────────────────────────────────  │
│                                                                              │
│  With Streaming (RSC + Suspense):                                           │
│  ────────────────────────────────                                           │
│  t=0      Server starts rendering                                           │
│  t=50ms   Shell HTML sent (layout, loading states)                          │
│  t=100ms  Browser starts painting!                                          │
│  t=500ms  First chunk streams (above-fold content)                          │
│  t=2000ms Slow query completes, chunk streams                               │
│  t=2050ms Final chunk, hydration completes                                  │
│                                                                              │
│  User sees content at 100ms, progressive enhancement                        │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Implementing Streaming

// app/dashboard/page.tsx

import { Suspense } from 'react';

export default async function Dashboard() {
  // Fast data — renders immediately
  const user = await getUser(); // 50ms

  return (
    <div>
      {/* Streams immediately */}
      <Header user={user} />

      {/* Streams when ready — shows loading first */}
      <Suspense fallback={<CardSkeleton />}>
        <RevenueCard />  {/* 200ms fetch */}
      </Suspense>

      {/* Independent stream — doesn't block RevenueCard */}
      <Suspense fallback={<TableSkeleton />}>
        <TransactionsTable />  {/* 800ms fetch */}
      </Suspense>

      {/* Nested streaming */}
      <Suspense fallback={<ChartLoading />}>
        <AnalyticsSection>
          <Suspense fallback={<MiniChartLoading />}>
            <ConversionChart />  {/* 1500ms fetch */}
          </Suspense>
        </AnalyticsSection>
      </Suspense>
    </div>
  );
}

// Each Suspense boundary streams independently
// Chunks arrive as they complete, not in order

The Actual HTTP Response

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked

<!-- First chunk: immediate -->
<!DOCTYPE html>
<html>
<head>...</head>
<body>
<div id="__next">
  <header>Welcome, John</header>
  <div id="revenue-loading">Loading revenue...</div>
  <div id="transactions-loading">Loading transactions...</div>
</div>
<script>self.__next_f.push([0])</script>
<script>self.__next_f.push([1,"0:[\"$\",\"div\"..."])</script>

<!-- Second chunk: RevenueCard ready (200ms later) -->
<script>self.__next_f.push([1,"1:[\"$\",\"$L5\"...revenue data..."])</script>
<script>$RC=function(b,c){...};$RC("revenue-loading","revenue-content")</script>
<div hidden id="revenue-content">
  <div class="card">Revenue: $50,000</div>
</div>

<!-- Third chunk: TransactionsTable ready (600ms later) -->
<script>self.__next_f.push([1,"2:[\"$\",\"$L8\"...table data..."])</script>
<script>$RC("transactions-loading","transactions-content")</script>
<div hidden id="transactions-content">
  <table>...</table>
</div>

<!-- Final chunk: ConversionChart ready (700ms later) -->
<script>self.__next_f.push([1,"3:[\"$\",\"$L12\"...chart data..."])</script>
<script>$RC("chart-loading","chart-content")</script>
<div hidden id="chart-content">
  <canvas>...</canvas>
</div>
</body>
</html>

The $RC function is React's "replace content" — it swaps the loading skeleton with the real content without a full re-render.


Phase 8: The Caching Layers

Next.js has four distinct caching layers. Understanding them prevents 90% of "why isn't my data updating" issues.

┌─────────────────────────────────────────────────────────────────────────────┐
│                    NEXT.JS CACHING ARCHITECTURE                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Layer 1: REQUEST MEMOIZATION (per-request)                                │
│  ───────────────────────────────────────────                                │
│  • Dedupes identical fetch calls in a single render                        │
│  • Only during initial server render                                        │
│  • Same URL + options = one actual fetch                                   │
│                                                                              │
│  // These make ONE network request                                          │
│  async function ComponentA() {                                              │
│    const data = await fetch('/api/user'); // Request 1                     │
│  }                                                                          │
│  async function ComponentB() {                                              │
│    const data = await fetch('/api/user'); // Reuses Request 1              │
│  }                                                                          │
│                                                                              │
│  ─────────────────────────────────────────────────────────────────────────  │
│                                                                              │
│  Layer 2: DATA CACHE (cross-request, server-side)                           │
│  ─────────────────────────────────────────────────                          │
│  • Persists fetch results across requests and deploys                      │
│  • Controlled by fetch options: cache, next.revalidate                     │
│  • Stored in: Vercel Data Cache, or file system (self-hosted)              │
│                                                                              │
│  await fetch('/api/data', {                                                 │
│    cache: 'force-cache',        // Use cache (default)                     │
│    // OR                                                                    │
│    cache: 'no-store',           // Skip cache                              │
│    // OR                                                                    │
│    next: { revalidate: 3600 },  // Cache for 1 hour                        │
│  });                                                                        │
│                                                                              │
│  ─────────────────────────────────────────────────────────────────────────  │
│                                                                              │
│  Layer 3: FULL ROUTE CACHE (pre-rendered pages)                            │
│  ───────────────────────────────────────────────                            │
│  • Caches the RSC payload + HTML at build time                             │
│  • For static and ISR pages                                                │
│  • Invalidated by: revalidatePath(), revalidateTag(), redeploy            │
│                                                                              │
│  Page type          Full Route Cache behavior                               │
│  ───────────────────────────────────────────                                │
│  Static             Cached at build, never invalidates                     │
│  ISR                Cached, revalidates per revalidate config              │
│  Dynamic            Not cached (rendered per request)                      │
│                                                                              │
│  ─────────────────────────────────────────────────────────────────────────  │
│                                                                              │
│  Layer 4: ROUTER CACHE (client-side)                                       │
│  ─────────────────────────────────────                                      │
│  • Browser memory cache of RSC payloads                                    │
│  • Enables instant back/forward navigation                                 │
│  • Prefetched routes cached for 30 seconds (static) or 0 (dynamic)        │
│  • Full page navigations cached for 5 minutes                              │
│  • Invalidated by: router.refresh(), revalidatePath(), cookies change     │
│                                                                              │
│  ⚠️ This is the most confusing cache — it's why you sometimes see         │
│     stale data after mutations until you hard refresh                       │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Cache Invalidation

// Server Action: Invalidate caches after mutation
'use server';

import { revalidatePath, revalidateTag } from 'next/cache';

export async function updatePost(postId: string, data: PostData) {
  // 1. Perform mutation
  await db.posts.update({ where: { id: postId }, data });

  // 2. Invalidate Data Cache entries with this tag
  revalidateTag('posts');        // All fetches tagged 'posts'
  revalidateTag(`post-${postId}`); // Specific post

  // 3. Invalidate Full Route Cache for these paths
  revalidatePath('/blog');             // Blog listing
  revalidatePath(`/blog/${postId}`);   // Specific post page
  revalidatePath('/blog/[slug]', 'page'); // All dynamic blog pages

  // 4. Router Cache automatically invalidated when:
  //    - revalidatePath is called
  //    - cookies are set/deleted
  //    - router.refresh() called on client
}

// Tagging fetches for granular invalidation
async function getPost(id: string) {
  const res = await fetch(`/api/posts/${id}`, {
    next: {
      tags: ['posts', `post-${id}`],  // Multiple tags
      revalidate: 3600,
    },
  });
  return res.json();
}

Phase 9: Client-Side Hydration

The HTML arrives. Now the browser takes over.

Hydration Process

┌─────────────────────────────────────────────────────────────────────────────┐
│                    HYDRATION TIMELINE                                        │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  t=0ms     HTML arrives, browser starts parsing                             │
│  t=50ms    First paint (HTML visible, not interactive)                      │
│  t=100ms   CSS loaded, styled paint                                         │
│  t=200ms   React runtime JS loaded                                          │
│  t=250ms   RSC payload parsed                                               │
│  t=300ms   Hydration begins                                                  │
│            • React walks the DOM                                             │
│            • Attaches event handlers                                         │
│            • Connects to state                                               │
│  t=400ms   Hydration complete — page is interactive                         │
│                                                                              │
│  SELECTIVE HYDRATION (with Suspense):                                       │
│  ─────────────────────────────────────                                       │
│  t=0ms     HTML arrives with loading skeletons                              │
│  t=50ms    First paint (shell visible)                                      │
│  t=200ms   React runtime loaded                                             │
│  t=250ms   Hydrate what's available (header, nav)                           │
│            User can interact with hydrated parts!                           │
│  t=400ms   Slow chunk streams in                                            │
│  t=450ms   Hydrate the new chunk                                            │
│                                                                              │
│  User can interact BEFORE full hydration completes                          │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

What Gets Sent to the Browser

┌─────────────────────────────────────────────────────────────────────────────┐
│                    BUNDLE ANALYSIS                                           │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  SENT TO BROWSER:                                                           │
│  ─────────────────                                                          │
│  ✓ HTML (initial render)                                                   │
│  ✓ RSC Payload (serialized component tree)                                 │
│  ✓ Client Component JavaScript                                              │
│  ✓ React runtime                                                            │
│  ✓ Next.js client runtime                                                   │
│  ✓ CSS (modules, global)                                                    │
│                                                                              │
│  NOT SENT:                                                                   │
│  ──────────                                                                 │
│  ✗ Server Component JavaScript                                              │
│  ✗ Server-only dependencies                                                │
│  ✗ Database clients                                                         │
│  ✗ File system code                                                         │
│  ✗ Environment secrets                                                      │
│                                                                              │
│  Example bundle impact:                                                      │
│  ──────────────────────                                                     │
│  Server Component using:              Not in bundle                         │
│    - prisma (45KB)                    ✗                                     │
│    - lodash (70KB)                    ✗                                     │
│    - date-fns (20KB)                  ✗                                     │
│                                                                              │
│  Client Component using:              In bundle                             │
│    - chart.js (60KB)                  ✓                                     │
│    - framer-motion (40KB)             ✓                                     │
│                                                                              │
│  Server Components keep your bundle small!                                  │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

The Complete Picture

┌─────────────────────────────────────────────────────────────────────────────┐
│                    END-TO-END REQUEST LIFECYCLE                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  1. BROWSER                                                                  │
│     └──▶ GET /dashboard                                                     │
│                                                                              │
│  2. CDN EDGE                                                                 │
│     ├── Cache HIT? ──▶ Return cached (10ms)                                │
│     └── Cache MISS ──▶ Continue to origin                                  │
│                                                                              │
│  3. MIDDLEWARE (Edge Runtime)                                               │
│     ├── Check auth, geolocation, A/B tests                                 │
│     ├── Redirect? ──▶ Return 307                                           │
│     ├── Rewrite? ──▶ Change internal routing                               │
│     └── Continue ──▶ Pass to route handler                                 │
│                                                                              │
│  4. ROUTE RESOLUTION                                                        │
│     └──▶ /app/dashboard/page.tsx                                           │
│                                                                              │
│  5. SERVER RENDER (Node.js or Edge)                                         │
│     ├── Execute Server Components                                           │
│     │   ├── Fetch data (with caching)                                      │
│     │   ├── Access database                                                │
│     │   └── Generate JSX                                                   │
│     ├── Pre-render Client Components (SSR)                                 │
│     └── Generate RSC payload                                                │
│                                                                              │
│  6. STREAMING                                                                │
│     ├── Send HTML shell + first chunks                                     │
│     ├── Stream Suspense boundaries as they resolve                         │
│     └── Complete when all boundaries resolve                               │
│                                                                              │
│  7. BROWSER RECEIVES                                                         │
│     ├── Parse HTML ──▶ First paint                                         │
│     ├── Load React + Client Components JS                                  │
│     ├── Parse RSC payload                                                   │
│     └── Hydrate ──▶ Interactive                                            │
│                                                                              │
│  8. SUBSEQUENT NAVIGATIONS                                                   │
│     ├── <Link> click ──▶ RSC request (not full HTML)                       │
│     ├── Prefetch on hover/viewport ──▶ Cache in Router Cache              │
│     └── Server Action ──▶ POST to server, revalidate caches               │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Debugging: Where Is My Code Running?

// Add this to any file to see where it runs

export function whereAmI() {
  const info = {
    isServer: typeof window === 'undefined',
    isEdge: typeof EdgeRuntime !== 'undefined',
    isNode: typeof process !== 'undefined' && process.versions?.node,
    isBrowser: typeof window !== 'undefined',
  };

  if (info.isEdge) return 'Edge Runtime';
  if (info.isNode && info.isServer) return 'Node.js Server';
  if (info.isBrowser) return 'Browser';
  return 'Unknown';
}

// In a Server Component
async function ServerComponent() {
  console.log('Rendering on:', whereAmI());  // "Node.js Server"
  return <div>Hello</div>;
}

// In a Client Component
'use client';
function ClientComponent() {
  console.log('Rendering on:', whereAmI());
  // First log: "Node.js Server" (SSR)
  // Second log: "Browser" (hydration)
  return <div>Hello</div>;
}

// In Middleware
export function middleware() {
  console.log('Running on:', whereAmI());  // "Edge Runtime"
  return NextResponse.next();
}

Common "Where Does This Run?" Questions

┌─────────────────────────────────────────────────────────────────────────────┐
│                    CODE LOCATION REFERENCE                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Code                                   Runs On                              │
│  ─────────────────────────────────────────────────────────────────────────  │
│  middleware.ts                          Edge Runtime (always)               │
│  Server Component                       Node.js (default) or Edge          │
│  Client Component (initial)             Node.js (SSR) + Browser (hydration)│
│  Client Component (navigation)          Browser only                        │
│  'use server' action                    Node.js (default) or Edge          │
│  route.ts (Route Handler)               Node.js (default) or Edge          │
│  generateStaticParams                   Node.js at build time              │
│  generateMetadata                       Node.js at request/build time      │
│  next.config.js                         Node.js at build time              │
│  instrumentation.ts                     Node.js at startup                 │
│                                                                              │
│  Runtime selection:                                                          │
│  ──────────────────                                                         │
│  export const runtime = 'edge';    // Forces Edge Runtime                  │
│  export const runtime = 'nodejs';  // Forces Node.js (default)             │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Performance Implications

┌─────────────────────────────────────────────────────────────────────────────┐
│                    ARCHITECTURAL PERFORMANCE LEVERS                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  LEVER                          IMPACT            HOW                        │
│  ─────────────────────────────────────────────────────────────────────────  │
│  Static generation              Fastest           export const dynamic =    │
│                                 (CDN cached)      'force-static'            │
│                                                                              │
│  ISR                            Fast              export const revalidate   │
│                                 (CDN + fresh)     = 60                      │
│                                                                              │
│  Streaming                      Fast TTFB         Use Suspense boundaries   │
│                                 (progressive)                               │
│                                                                              │
│  Edge Runtime                   Lower latency     export const runtime =    │
│                                 (near user)       'edge'                    │
│                                                                              │
│  Data caching                   Fewer fetches     fetch next: { revalidate }│
│                                                                              │
│  Request memoization            Fewer fetches     Automatic for same URL    │
│                                                                              │
│  Server Components              Smaller bundles   Default — avoid 'use      │
│                                                   client' where possible    │
│                                                                              │
│  Parallel data fetching         Lower latency     Promise.all([             │
│                                                     fetch1(),               │
│                                                     fetch2(),               │
│                                                   ])                        │
│                                                                              │
│  Prefetching                    Instant nav       <Link> auto-prefetches   │
│                                                                              │
│  ANTI-PATTERNS:                                                             │
│  ──────────────                                                             │
│  ✗ Dynamic when static works    Slower            Check if data changes    │
│  ✗ Edge when you need Node      Errors            Know runtime limits      │
│  ✗ Blocking waterfall fetches   Slow render       Use parallel + Suspense  │
│  ✗ Client Components for static Bigger bundle     Default to Server        │
│  ✗ No-cache everything          Slow + expensive  Use appropriate caching  │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Summary

A Next.js request isn't a simple HTTP round-trip. It's an orchestrated flow through:

  1. CDN Edge — Cache lookup, geographic routing
  2. Middleware — Edge-based request interception
  3. Route Resolution — File-system based matching
  4. Server Rendering — Components execute, data fetches
  5. RSC Serialization — Tree converted to wire format
  6. Streaming — Progressive HTML delivery
  7. Caching — Four layers, each with different semantics
  8. Hydration — Browser takes over

The architecture is complex because it solves hard problems: global latency, bundle size, caching, progressive enhancement. Understanding it means you can optimize for your specific use case — and debug the inevitable "why is this data stale?" issues.


The best Next.js apps are the ones where developers understand which code runs where, which cache affects what, and why their 50ms page became a 5000ms page. This knowledge separates "it works" from "it works well."

What did you think?

© 2026 Vidhya Sagar Thakur. All rights reserved.