Deep Dive Analysis: Complete flow from routing to rendering
Source: Next.js repository (vercel/next.js)
Analysis Date: October 15, 2025
- Overview
- Architecture Diagram
- Core Data Structures
- Request Flow
- Rendering Pipeline
- Key Patterns
- Performance Optimizations
- Development Guide
- Debugging & Tools
- Testing Strategies
- Common Pitfalls
- File Reference
Next.js implements React Server Components (RSC) through a sophisticated multi-layered architecture that seamlessly integrates routing, rendering, streaming, and hydration. The implementation leverages React's experimental APIs to create an efficient, streaming-first rendering pipeline.
- Streaming-First: Progressive delivery of HTML and data
- Dual Rendering Modes: Static generation with PPR or dynamic rendering
- Component-Level Code Splitting: Server components never reach the client
- Parallel Data Fetching: Multiple async operations execute simultaneously
- Granular Caching: Cache control at component and fetch level
graph TB
subgraph "Next.js Server"
BaseServer[Base Server<br/>Routing]
RouteModule[Route Module<br/>Matcher]
AppRenderer[App Renderer<br/>RSC Core]
ReactServer[React Server<br/>Components<br/>Rendering]
subgraph "Streaming Pipeline"
RSCPayload[RSC Payload<br/>Flight Format]
HTMLStream[HTML Stream<br/>SSR]
Progressive[Progressive<br/>Enhancement]
end
BaseServer --> RouteModule
RouteModule --> AppRenderer
AppRenderer --> ReactServer
ReactServer --> RSCPayload
RSCPayload --> HTMLStream
HTMLStream --> Progressive
end
Client[Client Browser] -.->|HTTP Request| BaseServer
Progressive -.->|Response| Client
style BaseServer fill:#e1f5ff
style AppRenderer fill:#fff4e1
style ReactServer fill:#ffe1f5
style Progressive fill:#e1ffe1
The fundamental structure representing the route hierarchy:
type LoaderTree = [
segment: string, // Route segment (e.g., "dashboard", "[id]")
parallelRoutes: { // Parallel routes (@modal, @sidebar)
[key: string]: LoaderTree
},
modules: { // Page components and metadata
layout?: [() => Promise<any>, string]
page?: [() => Promise<any>, string]
loading?: [() => Promise<any>, string]
error?: [() => Promise<any>, string]
'not-found'?: [() => Promise<any>, string]
}
]Purpose: Created at build time, encodes route structure, parallel routes, and lazy-loaded component modules.
Represents the active route state for client-side navigation:
type FlightRouterState = [
segment: Segment, // Current segment
parallelRoutes: { // Child routes
[key: string]: FlightRouterState
},
url?: string, // Full URL
refresh?: 'refetch', // Force refresh marker
isRootLayout?: boolean
]type FlightData = Array<[
...parallelRouteKeys: string[], // Path to segment
segment: Segment, // Target segment
treeState: FlightRouterState, // Router state
rscPayload: React.ReactNode, // Component tree
head: HeadData // Meta tags
]>sequenceDiagram
participant Client
participant BaseServer
participant RouteManifest
participant AppPageModule
participant Renderer
Client->>BaseServer: HTTP GET /dashboard/[id]
BaseServer->>BaseServer: Parse URL & Headers
BaseServer->>RouteManifest: Match Route
RouteManifest-->>BaseServer: AppPageRouteModule
BaseServer->>AppPageModule: Load Route Module
AppPageModule->>AppPageModule: Inject Vendored React<br/>(RSC/SSR versions)
AppPageModule->>AppPageModule: Set Vary Headers<br/>(RSC, State-Tree, Prefetch)
AppPageModule->>Renderer: render(req, res, context)
Renderer->>Renderer: Call renderToHTMLOrFlight()
Note over Renderer: Continue to Phase 2...
// Entry: renderToHTMLOrFlight()
// Location: packages/next/src/server/app-render/app-render.tsx
export const renderToHTMLOrFlight = (req, res, pagePath, ...) => {
// 1. Parse request headers
const parsedHeaders = parseRequestHeaders(req.headers)
// 2. Create async context
const workStore = createWorkStore({ page, renderOpts, ... })
// 3. Route to appropriate renderer
if (isStaticGeneration) {
return prerenderToStream(...)
} else {
return renderToStream(...)
}
}flowchart TD
Start([createComponentTree]) --> ParseTree[Parse LoaderTree Segment]
ParseTree --> ExtractParams["Extract Dynamic Parameters<br/>e.g., id from /dashboard/[id]"]
ExtractParams --> LoadModules[Load Layout/Page Modules<br/>Lazy-loaded from build]
LoadModules --> CreateHierarchy{Create React<br/>Element Hierarchy}
CreateHierarchy --> LayoutRouter[LayoutRouter<br/>Client-side navigation]
CreateHierarchy --> ServerRoot[ServerPageRoot/<br/>ServerSegmentRoot]
CreateHierarchy --> Boundaries[Error/Loading<br/>Boundaries]
LayoutRouter --> InjectAssets[Inject CSS/JS Assets]
ServerRoot --> InjectAssets
Boundaries --> InjectAssets
InjectAssets --> ParallelRoutes{Process Parallel<br/>Routes?}
ParallelRoutes -->|Yes: modal, sidebar| RecurseSlots[Recurse for Each Slot]
ParallelRoutes -->|None| GenerateMetadata
RecurseSlots --> GenerateMetadata[Generate Metadata<br/>Title, OG, etc.]
GenerateMetadata --> Return([Return CacheNodeSeedData<br/>React Tree])
style Start fill:#e1f5ff
style Return fill:#e1ffe1
style CreateHierarchy fill:#fff4e1
flowchart TB
Start([prerenderToStream<br/>Static Generation]) --> Phase1
subgraph Phase1["🔄 Phase 1: Prospective Prerender (Cache Filling)"]
P1Start[Create AbortControllers<br/>& CacheSignal] --> P1Render[Render Entire Component Tree]
P1Render --> P1Track[Track All Cache Reads<br/>fetch, unstable_cache, etc.]
P1Track --> P1Wait[Wait for Caches to Fill<br/>cacheSignal.cacheReady]
P1Wait --> P1Abort[Abort Controllers<br/>End prospective render]
end
Phase1 --> CheckError{Invalid Dynamic<br/>Usage?}
CheckError -->|Yes| ThrowError[Throw StaticGenBailoutError]
CheckError -->|No| Phase2
subgraph Phase2["✨ Phase 2: Final Prerender (Static Shell)"]
P2Start[Create New Controllers<br/>with Dynamic Tracking] --> P2Render[Render with Warm Caches<br/>ComponentMod.prerender]
P2Render --> P2Track[Track Dynamic API Usage<br/>cookies, headers, searchParams]
P2Track --> P2Postpone[Generate Postpone Markers<br/>for dynamic parts]
P2Postpone --> P2Stream[Produce RSC Stream]
end
Phase2 --> Phase3
subgraph Phase3["🎨 Phase 3: SSR HTML Generation"]
P3Start[Consume RSC Stream] --> P3Render[ReactDOMServer.renderToReadable<br/>Generate HTML]
P3Render --> P3Inline[Inline RSC Payload<br/>in script tags]
P3Inline --> P3Handle[Handle Postponed Boundaries<br/>Suspense placeholders]
end
Phase3 --> End([Return PrerenderResult<br/>with revalidate/tags/expire])
style Start fill:#e1f5ff
style End fill:#e1ffe1
style ThrowError fill:#ffe1e1
Key Code:
// React's prerender API for static generation
const result = await ComponentMod.prerender(
RSCPayload, // Component tree
clientReferenceManifest, // Client component mappings
{
onError: errorHandler,
onPostpone: postponeHandler, // Mark dynamic parts
signal: abortSignal
}
)flowchart TB
Start([renderToStream<br/>Dynamic Rendering]) --> Phase1
subgraph Phase1["🏗️ Phase 1: Build RSC Payload"]
P1Tree[Create Component Tree<br/>createComponentTree] --> P1Params[Resolve Dynamic Parameters<br/>from URL/cookies/headers]
P1Params --> P1Payload[Build RSC Payload<br/>getRSCPayload]
end
Phase1 --> Phase2
subgraph Phase2["🚀 Phase 2: Stream RSC"]
P2Create[Create Request Store<br/>No prerender tracking] --> P2Stream[ComponentMod.renderToReadableStream<br/>React Server Render]
P2Stream --> P2NoPostpone[No Postponing<br/>Full dynamic capabilities]
P2NoPostpone --> P2Result[ReactServerResult<br/>Wraps stream]
end
Phase2 --> Wait[Wait One React Render Task<br/>Allow preloads to register]
Wait --> Phase3
subgraph Phase3["🎨 Phase 3: SSR HTML Streaming"]
P3Check{Postponed State<br/>from Build?}
P3Check -->|Yes| P3Resume[Resume HTML Render<br/>ReactDOM.resume]
P3Check -->|No| P3Fresh[Fresh SSR Render<br/>ReactDOM.renderToPipeable]
P3Resume --> P3Consume[Consume RSC Stream<br/>via reactServerStream prop]
P3Fresh --> P3Consume
P3Consume --> P3Progressive[Progressive HTML Delivery<br/>Shell → Suspense boundaries]
P3Progressive --> P3Chain[Chain Streams<br/>initial + continuation + closing]
end
Phase3 --> End([Return RenderResult<br/>HTML + RSC stream])
style Start fill:#e1f5ff
style End fill:#e1ffe1
style Wait fill:#fff4e1
Key Code:
// Dynamic RSC rendering
const rscStream = ComponentMod.renderToReadableStream(
RSCPayload,
clientReferenceManifest.clientModules,
{ onError, debugChannel }
)
// SSR with RSC consumption
const htmlStream = ReactDOMServer.renderToPipeableStream(
<App reactServerStream={rscStream.tee()} />,
{ onShellReady, onAllReady, onError }
)Static and dynamic content in the same page:
export default async function Page() {
return (
<div>
<StaticHeader /> {/* Prerendered */}
<Suspense fallback={<Skeleton />}>
<DynamicContent /> {/* Postponed, rendered on request */}
</Suspense>
<StaticFooter /> {/* Prerendered */}
</div>
)
}Flow:
- Build: Generate static shell, mark Suspense as postponed
- Request: Resume only dynamic parts
- Stream dynamic content into placeholders
// Server Component
export default function UserProfile({ userId }) {
const userPromise = fetchUser(userId) // Starts immediately
return (
<Suspense fallback={<Loading />}>
<UserDetails userPromise={userPromise} />
</Suspense>
)
}
// Child component
function UserDetails({ userPromise }) {
const user = use(userPromise) // Suspends until resolved
return <div>{user.name}</div>
}// Server Component
export default async function Page() {
const data = await fetchData() // Server-only
return (
<ClientComponent data={data}> {/* Serialized */}
<ServerComponent /> {/* Rendered as RSC */}
</ClientComponent>
)
}// Maintain context across async boundaries
workAsyncStorage.run(workStore, () => {
workUnitAsyncStorage.run(requestStore, () => {
// All operations have access to:
// - Dynamic API state
// - Cache configuration
// - Prerender tracking
})
})- HTML starts before React finishes
- Suspense enables progressive delivery
- Critical resources injected early
// All fetches start simultaneously
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
])- Prospective prerender: Fill caches before final render
- Revalidation tags: Granular invalidation
- Resume data cache: PPR continuation data
- Server components: Never bundled for client
- Client components: Lazy loaded on demand
- Shared dependencies: Automatically deduplicated
// Chain multiple streams efficiently
chainStreams([
initialFizzStream, // Shell + initial content
continuationStream, // Suspense resolutions
closingStream // Document closing tags
])Binary format encoding React component tree:
M1:{"id":"123","name":"Product"} // Module reference
S2:"loading" // String chunk
J0:["$","div",null,{...}] // JSON (React element)
sequenceDiagram
participant User
participant NextLink as Link Component
participant Router
participant Cache
participant Server
participant DOM
User->>NextLink: Click link to /new-page
NextLink->>Router: Intercept Navigation
Router->>Router: Check Prefetch Cache
alt Cache Hit
Router->>Cache: Get Cached FlightData
Cache-->>Router: Return Cached Data
else Cache Miss
Router->>Server: GET /new-page
Note over Router,Server: Headers:<br/>RSC: 1<br/>Next-Router-State-Tree: [...]
Server->>Server: Generate RSC Payload
Server-->>Router: FlightData Response
Router->>Cache: Store in Cache
end
Router->>Router: Update URL (Optimistic)
Router->>Router: Apply New Router State
Router->>DOM: Render New RSC Payload
Router->>DOM: Update <head> (meta, title)
Router->>DOM: Scroll to Top/Hash
DOM-->>User: Updated Page Visible
Note over User,DOM: Navigation Complete
// During prerender, track dynamic API usage
export function trackDynamicData(store: WorkStore) {
if (store.isStaticGeneration) {
store.dynamicUsageDescription = 'cookies() was called'
if (store.forceDynamic) {
throw new DynamicServerError('Route is dynamic')
}
}
}graph TD
Link[Link Component] --> Viewport{In Viewport?}
Viewport -->|Yes| DefaultPrefetch[Default Prefetch<br/>Full Route Tree]
Viewport -->|No| OnHover[Prefetch on Hover]
DefaultPrefetch --> Cache1[Store in Router Cache]
OnHover --> Cache1
Cache1 --> Duration{Cache Duration}
Duration -->|Static| Long[30 seconds]
Duration -->|Dynamic| Short[30 seconds from last access]
style DefaultPrefetch fill:#e1f5ff
style Cache1 fill:#fff4e1
Implementation:
// Disable prefetching
<Link href="/page" prefetch={false}>No Prefetch</Link>
// Force prefetch (even outside viewport)
<Link href="/page" prefetch={true}>Always Prefetch</Link>
// Default behavior (null)
<Link href="/page">Auto Prefetch in Viewport</Link>import { cache } from 'react'
// Create cached function - deduplicates within single request
const getUser = cache(async (id: string) => {
console.log('Fetching user:', id) // Only logs once per request
return await db.user.findUnique({ where: { id } })
})
// Layout
export default async function Layout({ children }) {
const user = await getUser('123') // First call
return <div>{children}</div>
}
// Page (in same request)
export default async function Page() {
const user = await getUser('123') // Uses cached result!
return <div>{user.name}</div>
}// app/blog/[slug]/page.tsx
export const revalidate = 60 // Revalidate every 60 seconds
export default async function BlogPost({ params }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: { revalidate: 60 }
})
return <article>{/* Render post */}</article>
}
// Or use tags for on-demand revalidation
export default async function BlogPost({ params }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: { tags: [`post-${params.slug}`] }
})
return <article>{/* Render post */}</article>
}
// Revalidate from API route
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache'
export async function POST(request: Request) {
const { tag } = await request.json()
revalidateTag(tag)
return Response.json({ revalidated: true })
}// Advanced streaming pattern
export default function Page() {
// Start all fetches immediately
const userPromise = fetchUser()
const postsPromise = fetchPosts()
const commentsPromise = fetchComments()
return (
<div>
{/* These all stream in independently */}
<Suspense fallback={<UserSkeleton />}>
<UserProfile promise={userPromise} />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<PostsList promise={postsPromise} />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments promise={commentsPromise} />
</Suspense>
</div>
)
}
function UserProfile({ promise }: { promise: Promise<User> }) {
const user = use(promise) // Suspends until resolved
return <div>{user.name}</div>
}// Static metadata
export const metadata = {
title: 'My Page',
description: 'Page description',
}
// Dynamic metadata
export async function generateMetadata({ params }) {
const product = await fetchProduct(params.id)
return {
title: product.title,
description: product.description,
openGraph: {
images: [product.image],
},
}
}
// Dynamic metadata with streaming
export async function generateMetadata({ params }) {
// This will be part of initial HTML shell
return {
title: 'Loading...',
}
}// app/actions.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title')
const content = formData.get('content')
await db.post.create({
data: { title, content }
})
revalidatePath('/blog')
redirect('/blog')
}
// app/blog/new/page.tsx
import { createPost } from '../actions'
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" />
<textarea name="content" />
<button type="submit">Create</button>
</form>
)
}// app/page.tsx
// Configure entire route segment
export const dynamic = 'force-dynamic' // 'auto' | 'force-dynamic' | 'error' | 'force-static'
export const dynamicParams = true // true | false
export const revalidate = false // false | 0 | number
export const fetchCache = 'auto' // 'auto' | 'default-cache' | 'only-cache' | 'force-cache' | 'force-no-store' | 'default-no-store' | 'only-no-store'
export const runtime = 'nodejs' // 'nodejs' | 'edge'
export const preferredRegion = 'auto' // 'auto' | 'global' | 'home' | string | string[]
export default async function Page() {
return <div>Page</div>
}// Edge Runtime (faster cold starts, limited APIs)
export const runtime = 'edge'
export default function Page() {
// Can't use Node.js APIs like fs, path
// Can use Web APIs: fetch, Request, Response
return <div>Edge Page</div>
}
// Node.js Runtime (default, full APIs)
export const runtime = 'nodejs'
import fs from 'fs'
import path from 'path'
export default async function Page() {
// Full Node.js API access
const data = fs.readFileSync(path.join(process.cwd(), 'data.json'))
return <div>{data}</div>
}mindmap
root((Next.js RSC<br/>Best Practices))
Component Design
Keep Server Components at Top Level
Use Client Components for Interactivity
Minimize Client Component Size
Pass Data Down as Props
Data Fetching
Fetch Close to Where Used
Use React cache for Deduplication
Set Explicit Cache Strategies
Use Streaming for Slow Data
Performance
Enable PPR When Possible
Use Parallel Data Fetching
Implement Proper Loading States
Optimize Images and Assets
Caching
Tag Important Resources
Set Appropriate Revalidation
Use ISR for Semi-Static Content
Leverage Router Cache
Error Handling
Use Error Boundaries
Implement Not Found Pages
Handle Loading States
Provide Fallback UI
flowchart TD
Start{Need Interactivity?}
Start -->|No| ServerComp[Use Server Component<br/>✓ Better performance<br/>✓ Direct DB access<br/>✓ SEO friendly]
Start -->|Yes| ClientCheck{Need Server Data?}
ClientCheck -->|No| PureClient[Pure Client Component<br/>✓ useState, useEffect<br/>✓ Event handlers<br/>✓ Browser APIs]
ClientCheck -->|Yes| Hybrid[Hybrid Approach<br/>Server Component + Client Child]
Hybrid --> FetchWhere{Where to Fetch?}
FetchWhere -->|Server| ServerFetch[Fetch in Server Component<br/>Pass props to Client]
FetchWhere -->|Client| ClientFetch[Use SWR/React Query<br/>in Client Component]
ServerComp --> NeedDynamic{Need Dynamic<br/>Data?}
NeedDynamic -->|No| Static[Static Generation<br/>export const revalidate = 3600]
NeedDynamic -->|Yes| Dynamic[Dynamic Rendering<br/>Use cookies/headers]
Dynamic --> SlowData{Has Slow<br/>Data?}
SlowData -->|Yes| Streaming[Use Streaming<br/>Wrap in Suspense]
SlowData -->|No| NoStreaming[Regular Render]
style ServerComp fill:#e1f5ff
style PureClient fill:#ffe1f5
style Hybrid fill:#fff4e1
style Streaming fill:#e1ffe1
| Component | File Path |
|---|---|
| Entry Point | packages/next/src/server/route-modules/app-page/module.ts |
| Main Renderer | packages/next/src/server/app-render/app-render.tsx |
| Component Tree | packages/next/src/server/app-render/create-component-tree.tsx |
| Tree Walking | packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx |
| Streaming Utils | packages/next/src/server/stream-utils/node-web-streams-helper.ts |
| React Integration | packages/next/src/server/app-render/entry-base.ts |
| LoaderTree Type | packages/next/src/server/lib/app-dir-module.ts |
| Prerender Utils | packages/next/src/server/app-render/app-render-prerender-utils.ts |
| Flight Result | packages/next/src/server/app-render/flight-render-result.ts |
graph TB
Start(["HTTP Request<br/>GET /dashboard/[id]"]) --> BaseServer
subgraph "Request Processing"
BaseServer[BaseServer<br/>Route Matching] --> ParseURL[Parse URL & Headers<br/>RSC/Prefetch/Full]
ParseURL --> LoadModule[Load AppPageRouteModule]
LoadModule --> RenderEntry[renderToHTMLOrFlight]
RenderEntry --> CreateContext[Create Work Stores<br/>Async Context]
CreateContext --> LoadTree[Load LoaderTree<br/>from Build]
end
LoadTree --> Decision{Rendering<br/>Mode?}
Decision -->|Static Gen| StaticPath
Decision -->|Dynamic| DynamicPath
subgraph StaticGen["Static Generation Path"]
StaticPath[prerenderToStream] --> S1[1. Cache Filling<br/>Prospective Prerender]
S1 --> S2[2. Final Prerender<br/>with Postpone Tracking]
S2 --> S3[3. Generate Static Shell<br/>+ HTML with Placeholders]
end
subgraph DynamicRender["Dynamic Rendering Path"]
DynamicPath[renderToStream] --> D1[1. Build Component Tree<br/>Resolve Dynamic Params]
D1 --> D2[2. Render RSC Stream<br/>Full Capabilities]
D2 --> D3[3. SSR HTML Stream<br/>Progressive Delivery]
end
S3 --> Merge[Merge Streams]
D3 --> Merge
Merge --> Response[Response Sent to Client]
Response --> Assets[• HTML Stream<br/>• RSC Payload<br/>• JS/CSS Assets]
Assets --> End([Client Receives<br/>& Hydrates])
style Start fill:#e1f5ff
style End fill:#e1ffe1
style Decision fill:#fff4e1
style StaticGen fill:#f0f0ff
style DynamicRender fill:#fff0f0
# Clone Next.js repository
git clone https://github.com/vercel/next.js.git
cd next.js
# Install dependencies
pnpm install
# Build Next.js
pnpm build
# Run tests
pnpm test
# Start development with example app
cd examples/app-dir-basic
pnpm dev# Enable verbose logging for RSC
NEXT_PRIVATE_DEBUG_CACHE=1
# Enable PPR debugging
NEXT_DEBUG_BUILD=1
# Verbose logging for development
__NEXT_VERBOSE_LOGGING=1
# Enable React experimental features
NEXT_PRIVATE_REACT_ROOT=1flowchart LR
Code[Write Code] --> Build[Build Next.js<br/>pnpm build]
Build --> Link[Link Package<br/>pnpm link]
Link --> TestApp[Test in App]
TestApp --> Debug{Issues?}
Debug -->|Yes| Investigate[Investigate with<br/>Chrome DevTools]
Debug -->|No| Commit[Commit Changes]
Investigate --> Code
style Code fill:#e1f5ff
style Commit fill:#e1ffe1
// app/components/ServerComponent.tsx
import { headers, cookies } from 'next/headers'
export default async function ServerComponent() {
// Access server-only APIs
const headersList = headers()
const cookieStore = cookies()
// Fetch data server-side
const data = await fetch('https://api.example.com/data', {
cache: 'no-store', // Dynamic
// OR
next: { revalidate: 60 } // Revalidate every 60s
})
return <div>{/* Render data */}</div>
}// app/page.tsx
import { Suspense } from 'react'
export default function Page() {
return (
<div>
{/* This renders immediately */}
<StaticContent />
{/* This streams in when ready */}
<Suspense fallback={<LoadingSkeleton />}>
<AsyncComponent />
</Suspense>
</div>
)
}
async function AsyncComponent() {
// This fetch will cause Suspense to trigger
const data = await fetch('https://api.example.com/slow', {
cache: 'no-store'
})
return <div>{JSON.stringify(data)}</div>
}app/
@modal/
photo/[id]/
page.tsx
@feed/
page.tsx
layout.tsx
page.tsx
// app/layout.tsx
export default function Layout({
children,
modal,
feed
}: {
children: React.ReactNode
modal: React.ReactNode
feed: React.ReactNode
}) {
return (
<>
{children}
{modal}
{feed}
</>
)
}// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
// Access search params
const searchParams = request.nextUrl.searchParams
const id = searchParams.get('id')
// Fetch data
const users = await fetchUsers(id)
// Return JSON
return NextResponse.json(users)
}
export async function POST(request: NextRequest) {
const body = await request.json()
// Process data
const result = await createUser(body)
return NextResponse.json(result, { status: 201 })
}flowchart TD
Start[Open DevTools] --> Network[Network Tab]
Network --> Filter[Filter: RSC=1]
Filter --> Inspect[Inspect Response]
Inspect --> Payload[View RSC Payload<br/>M: Module refs<br/>S: Strings<br/>J: JSON]
Start --> Sources[Sources Tab]
Sources --> Breakpoints[Set Breakpoints in<br/>Server Components]
Start --> Console[Console Tab]
Console --> Logs[View Server Logs<br/>console.log in SC]
style Payload fill:#fff4e1
Add to your app for development:
// app/components/RSCDebugger.tsx
'use client'
export function RSCDebugger({ payload }: { payload: any }) {
if (process.env.NODE_ENV !== 'development') return null
return (
<details style={{
position: 'fixed',
bottom: 0,
right: 0,
background: 'white',
border: '1px solid black',
padding: '10px',
maxWidth: '500px',
maxHeight: '300px',
overflow: 'auto'
}}>
<summary>RSC Debug Info</summary>
<pre>{JSON.stringify(payload, null, 2)}</pre>
</details>
)
}// Enable in next.config.js
module.exports = {
experimental: {
isrFlushToDisk: true,
logging: {
level: 'verbose',
fullUrl: true
}
}
}# Install React DevTools extension
# Then enable in Next.js
# In your component
import { useDebugValue } from 'react'
function MyComponent() {
useDebugValue('Debug info visible in DevTools')
// ...
}# Verbose build output
NEXT_DEBUG_BUILD=1 pnpm build
# Analyze bundle
npm install -g @next/bundle-analyzer
# Then in next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({
// your config
})
# Run with analysis
ANALYZE=true pnpm build// app/components/ProfiledComponent.tsx
import { unstable_trace as trace } from 'next/server'
export default async function ProfiledComponent() {
return trace('my-component', async () => {
const data = await fetchData()
return <div>{data}</div>
})
}// app/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<details>
<summary>Error Details (Dev Only)</summary>
<pre>{error.message}</pre>
<pre>{error.stack}</pre>
{error.digest && <p>Error Digest: {error.digest}</p>}
</details>
<button onClick={reset}>Try again</button>
</div>
)
}// __tests__/ServerComponent.test.tsx
import { render } from '@testing-library/react'
import ServerComponent from '@/app/components/ServerComponent'
// Mock server-only modules
jest.mock('next/headers', () => ({
headers: () => new Headers(),
cookies: () => ({
get: jest.fn(),
set: jest.fn(),
})
}))
describe('ServerComponent', () => {
it('renders without crashing', async () => {
const Component = await ServerComponent()
const { container } = render(Component)
expect(container).toMatchSnapshot()
})
})// e2e/app.spec.ts
import { test, expect } from '@playwright/test'
test('server component renders correctly', async ({ page }) => {
await page.goto('/dashboard')
// Wait for hydration
await page.waitForLoadState('networkidle')
// Check server-rendered content
const content = await page.textContent('h1')
expect(content).toBe('Dashboard')
// Test streaming
await expect(page.locator('[data-suspense]')).toBeVisible()
})// __tests__/rsc-payload.test.ts
import { renderToReadableStream } from 'react-server-dom-webpack/server'
import { createFromReadableStream } from 'react-server-dom-webpack/client'
test('RSC payload serialization', async () => {
const payload = <MyServerComponent />
const stream = renderToReadableStream(
payload,
clientManifest
)
const result = await createFromReadableStream(stream)
expect(result).toBeDefined()
})// __tests__/caching.test.ts
import { unstable_cache } from 'next/cache'
describe('Cache behavior', () => {
const cachedFn = unstable_cache(
async (id: string) => {
return `data-${id}`
},
['test-cache'],
{ revalidate: 60 }
)
it('caches results', async () => {
const result1 = await cachedFn('123')
const result2 = await cachedFn('123')
expect(result1).toBe(result2)
})
})// __tests__/performance.test.ts
import { unstable_trace as trace } from 'next/server'
test('component render time', async () => {
const start = performance.now()
await trace('test-component', async () => {
const component = await MyComponent()
render(component)
})
const duration = performance.now() - start
expect(duration).toBeLessThan(100) // 100ms threshold
})❌ Wrong:
// app/page.tsx (Server Component)
export default function Page() {
const [state, setState] = useState(0) // Error!
useEffect(() => {
// Error! Server components can't use hooks
}, [])
return <div onClick={() => {}}> {/* Error! No event handlers */}</div>
}✅ Correct:
// app/page.tsx (Server Component)
import ClientButton from './ClientButton'
export default async function Page() {
const data = await fetchData()
return (
<div>
<ClientButton data={data} />
</div>
)
}
// app/ClientButton.tsx
'use client'
export default function ClientButton({ data }) {
const [state, setState] = useState(0) // ✓ OK in client component
return <button onClick={() => setState(s => s + 1)}>{state}</button>
}❌ Wrong:
// Passing non-serializable data to client component
<ClientComponent
date={new Date()} // Error! Can't serialize Date
fn={() => {}} // Error! Can't serialize functions
symbol={Symbol()} // Error! Can't serialize symbols
/>✅ Correct:
<ClientComponent
dateString={new Date().toISOString()} // ✓ Serialize as string
onClick={undefined} // ✓ Define handler in client
/>❌ Wrong:
export default async function Page() {
const staticData = await fetch('https://api.example.com/static', {
next: { revalidate: 3600 }
})
// This makes the ENTIRE route dynamic!
const userId = cookies().get('userId')
// Static data now re-fetched on every request
return <div>...</div>
}✅ Correct:
export default async function Page() {
const staticData = await fetch('https://api.example.com/static', {
next: { revalidate: 3600 }
})
return (
<div>
<StaticContent data={staticData} />
<Suspense fallback={<Loading />}>
<DynamicContent /> {/* Isolated dynamic part */}
</Suspense>
</div>
)
}
async function DynamicContent() {
const userId = cookies().get('userId')
// Only this part is dynamic
}❌ Wrong:
// Expecting fresh data but caching indefinitely
const data = await fetch('https://api.example.com/data')
// Default: { cache: 'force-cache' }✅ Correct:
// Explicitly set cache behavior
const data = await fetch('https://api.example.com/data', {
cache: 'no-store', // Always fresh
// OR
next: { revalidate: 60 } // Revalidate every 60s
})❌ Wrong:
export default async function Page() {
const data = await fetchData() // Unhandled error crashes entire page
return <div>{data}</div>
}✅ Correct:
export default async function Page() {
return (
<ErrorBoundary fallback={<ErrorUI />}>
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
</ErrorBoundary>
)
}
async function DataComponent() {
try {
const data = await fetchData()
return <div>{data}</div>
} catch (error) {
throw new Error('Failed to load data')
}
}❌ Wrong:
// Storing large objects in async storage
workStore.largeData = new Array(1000000).fill('data')✅ Correct:
// Store only necessary metadata
workStore.dataId = 'ref-123'
// Fetch full data when needed❌ Wrong:
// Not coordinating parallel fetches
export default async function Layout({ modal, sidebar }) {
await fetchSharedData() // Fetched twice
return (
<>
{modal} {/* Also fetches shared data */}
{sidebar} {/* Also fetches shared data */}
</>
)
}✅ Correct:
// Use React cache for deduplication
import { cache } from 'react'
const getSharedData = cache(async () => {
return await fetchSharedData()
})
// Now all components share the same cached result❌ Wrong:
// Using hooks without 'use client'
export default function Component() {
const [state, setState] = useState(0)
// Error: You're importing a component that needs useState
return <div>{state}</div>
}✅ Correct:
'use client'
export default function Component() {
const [state, setState] = useState(0)
return <div>{state}</div>
}flowchart TD
Issue[Encountering Issue] --> Type{Issue Type?}
Type -->|Rendering| R1[Check Component Type<br/>Server vs Client]
R1 --> R2[Verify 'use client' directive]
R2 --> R3[Check for server-only APIs]
Type -->|Data| D1[Check fetch cache options]
D1 --> D2[Verify revalidation settings]
D2 --> D3[Check for dynamic APIs]
Type -->|Performance| P1[Enable verbose logging]
P1 --> P2[Profile with trace API]
P2 --> P3[Check bundle size]
Type -->|Hydration| H1[Check for Date/Random values]
H1 --> H2[Verify SSR/CSR parity]
H2 --> H3[Check useEffect usage]
R3 --> Resolve[Resolve Issue]
D3 --> Resolve
P3 --> Resolve
H3 --> Resolve
style Issue fill:#ffe1e1
style Resolve fill:#e1ffe1
Next.js Server Components architecture represents a sophisticated server-client rendering system that:
- Unifies static and dynamic rendering through a single pipeline
- Leverages React's experimental APIs (prerender, renderToReadableStream)
- Enables streaming by default for optimal performance
- Provides granular control over caching and revalidation
- Maintains async context via AsyncLocalStorage for magical APIs
- Splits code automatically between server and client
This architecture enables developers to build highly performant applications with optimal SEO, while maintaining the flexibility to use dynamic data when needed—all with minimal configuration and maximum developer experience.
Analysis Source: Next.js Repository
Primary Branch: canary
Key Version: Latest (as of October 2025)