Architecture Overview
Server Runtime
Rendering Pipeline
Client Navigation
Caching and Export
Development Tools
Build and Configuration
Ecosystem Packages
Testing Infrastructure
How It Works
The following files were used as context for generating this wiki page:
App Server Rendering coordinates the server-side generation, request processing, and React Server Component (RSC) streaming lifecycle for Next.js App Router applications. It bridges base server request reception and route dispatch with nested component tree assembly, hierarchical router state traversal, and specialized execution pipelines like Server Actions and instant validation. By orchestrating payload serialization and segment data collection across static, runtime, and dynamic rendering phases, the system delivers precise incremental updates and hydrated client states.
Sources: packages/next/src/server/app-render/app-render.tsx:2471-2485, packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx:28-56, packages/next/src/client/app-index.tsx:349-388, packages/next/src/server/app-render/action-handler.ts:538-558
Request routing and dispatch bridge incoming HTTP requests from BaseServer into the application render pipeline defined in app-render.tsx. When a request arrives, BaseServer matches route patterns utilizing route matcher providers such as AppPageRouteMatcherProvider, AppRouteRouteMatcherProvider, and pages manifest loaders [packages/next/src/server/base-server.ts:98-103]. Once matched, dispatch mechanisms initialize metadata, parse request headers, configure work and request stores, and invoke renderToHTMLOrFlightImpl to produce either rendered HTML or serialized Flight RSC payloads [packages/next/src/server/app-render/app-render.tsx:2471-2485].
Sources: packages/next/src/server/base-server.ts:98-103, packages/next/src/server/app-render/app-render.tsx:2471-2485
The execution sequence from initial request reception to stream generation flows through specific core functions:
BaseServer matches incoming URL paths using provider managers and loads route component modules.renderToHTMLOrFlightImpl receives req, res, url, pagePath, query, and renderOpts, setting initial status codes (such as 404 for /404 paths) and unique request timestamps [packages/next/src/server/app-render/app-render.tsx:2471-2488, 2495].ComponentMod.patchFetch() is executed, and module loaders extract loaderTree properties from routeModule.userland [packages/next/src/server/app-render/app-render.tsx:2571-2577].flightRouterState, isPrefetchRequest, isRSCRequest, isHmrRefresh) are extracted to determine request classification [packages/next/src/server/app-render/app-render.tsx:2599-2607].requestId and htmlRequestId) are generated or parsed from headers via crypto hashing or nanoid [packages/next/src/server/app-render/app-render.tsx:2609-2632].getImplicitTags with resolvedPathname, and an AppRenderContext (ctx) object is constructed [packages/next/src/server/app-render/app-render.tsx:2644-2678].workStore.isStaticGeneration evaluates whether to trigger prerenderToStream or direct dynamic rendering [packages/next/src/server/app-render/app-render.tsx:2594, 2682-2694].Note
Request IDs are derived using different strategies based on execution context: cryptographic SHA-1 hashes of request URLs during static generation, crypto.randomUUID() in Edge runtimes, and nanoid() in Node.js runtimes.
Next.js parses hierarchical loader trees (LoaderTree) to construct the nested React Server Component tree and cache node seed data. The loader tree utility parseLoaderTree extracts route segments, parallel routes, and module conventions from the tree array structure, identifying the active segment key and resolving convention paths for layouts, templates, and pages.
The loader tree structure tuple contains four primary elements: [segment, parallelRoutes, modules, staticSiblings]. The helper function parseLoaderTree extracts these fields, checking if the segment matches DEFAULT_SEGMENT_KEY (__DEFAULT__) to fall back to modules.defaultPage. The active convention path is resolved by checking layout?.[1] || template?.[1] || page?.[1].
Root and segment parameters are computed via recursive helper implementations that traverse down parallel route children until the root layout is reached.
export function getRootParams(
loaderTree: LoaderTree,
getDynamicParamFromSegment: GetDynamicParamFromSegment
): Params {
return getRootParamsImpl({}, loaderTree, getDynamicParamFromSegment)
}Component trees are wrapped and serialized into CacheNodeSeedData tuples via createSeedData. If segments are not runtime-prefetchable, rendering is deferred by awaiting the first late render stage (FIRST_LATE_RENDER_STAGE). When loading boundaries are present, LoadingBoundaryProvider wraps the React node element.
function createSeedData(
ctx: AppRenderContext,
rsc: React.ReactNode,
parallelRoutes: Record<string, CacheNodeSeedData | null>,
loading: LoadingModuleData | null,
isPossiblyPartialResponse: boolean,
isRuntimePrefetchable: boolean,
varyParamsAccumulator: VaryParamsAccumulator | null
): CacheNodeSeedData {
const createElement = ctx.componentMod.createElement
if (!isRuntimePrefetchable) {
const workUnitStore = workUnitAsyncStorage.getStore()
if (workUnitStore) {
let stagedRendering: StagedRenderingController | null | undefined
switch (workUnitStore.type) {
case 'request':
case 'prerender-runtime':
stagedRendering = workUnitStore.stagedRendering
if (stagedRendering) {
const deferredRsc = rsc
rsc = stagedRendering
.waitForStage(FIRST_LATE_RENDER_STAGE)
.then(() => deferredRsc)
}
break
case 'prerender':
case 'prerender-client':
case 'validation-client':
case 'prerender-ppr':
case 'prerender-legacy':
case 'cache':
case 'private-cache':
case 'unstable-cache':
case 'generate-static-params':
break
default:
workUnitStore satisfies never
}
}
}
if (loading !== null) {
const LoadingBoundaryProvider = ctx.componentMod.LoadingBoundaryProvider
rsc = createElement(LoadingBoundaryProvider, {
loading: loading,
children: rsc,
})
}
return [
rsc,
parallelRoutes,
null,
isPossiblyPartialResponse,
varyParamsAccumulator ? getVaryParamsThenable(varyParamsAccumulator) : null,
]
}Note
createSeedData returns a tuple structure [rsc, parallelRoutes, null, isPossiblyPartialResponse, varyParamsAccumulatorThenable] which forms the base seed data consumed by client-side router caches during navigation and prefetching.
The walkTreeWithFlightRouterState function traverses both the server-side loaderTreeToFilter and the client-side flightRouterState simultaneously. This dual-tree walk determines at what common layout or split point differential rendering must begin for client navigations or prefetches.
The traversal inspects each node of the loader tree, matching segments and evaluating router markers to decide whether to skip component rendering, emit metadata only, or trigger component tree generation at the current level.
const renderComponentsOnThisLevel =
!flightRouterState ||
!matchSegment(actualSegment, flightRouterState[0]) ||
flightRouterState[3] === 'refetch'When client-side navigation or prefetching requests specific subsets of the tree, flightRouterState[3] carries control strings that modify traversal behavior.
Warning
If PPR is disabled and a prefetch request encounters no loading component in the tree, traversal short-circuits immediately and returns only the router state, avoiding expensive sub-tree renders.
Server actions enable client-initiated mutations executed on the server. The handleAction function in action-handler.ts manages incoming server action invocations, request forwarding, payload decoding, revalidations, and state synchronization across workers.
When an action request is identified, execution flows through a precise sequence of steps from request validation to action resolution and post-execution cleanup:
handleAction() → getServerActionMetadata() → selectWorkerForForwarding() → actionAsyncStorage.run() → decodeReply() / decodeReplyFromBusboy() → executeActionAndPrepareForRender() → synchronizeMutableCookies() → executeRevalidates()
Before executing an action, handleAction verifies the request origin against the host header to prevent Cross-Site Request Forgery (CSRF). If an action ID belongs to a different worker thread, the request is forwarded via createForwardedActionResponse.
if (!originHost) {
warning = 'Missing `origin` header from a forwarded Server Actions request.'
} else if (!host || originHost !== host.value) {
if (isCsrfOriginAllowed(originHost, serverActions?.allowedOrigins)) {
// Ignore it
} else {
const error = new Error('Invalid Server Actions request.')
throw error
}
}Caution
If a server action is invoked during static rendering (workStore.isStaticGeneration), handleAction immediately throws an invariant error because mutations are disallowed at build time.
Upon successful completion of an action handler, executeActionAndPrepareForRender transitions the request store phase from 'action' to 'render', synchronizes mutable cookies, and processes pending revalidations.
Note
Revalidation headers like x-action-revalidated are populated on the response via addRevalidationHeader depending on whether tags, cookies, or paths were invalidated.
Segment data extraction and instant validation parse route payloads and manage prefetch segment streams. The subsystem relies on utilities like traverseRootSeedDataSegments to recursively walk router states, coordinate seed data extraction, and process individual route nodes during prefetches.
The server plans validation workflows by converting segments and route trees into serializable paths. Traversal starts at the root payload, passing each segment path and seed data structure down through child routes via parallel route keys.
function traverseCacheNodeSegments(
path: SegmentPath,
route: FlightRouterState,
seedData: CacheNodeSeedData,
processSegment: (
segmentPath: SegmentPath,
seedData: CacheNodeSeedData
) => void
): void {
processSegment(path, seedData)
const [_segment, childRoutes] = route
const [_node, parallelRoutesData, _loading, _isPartial] = seedData
for (const parallelRouteKey in childRoutes) {
const childSeedData = parallelRoutesData[parallelRouteKey]
if (!childSeedData) {
throw new InvariantError(
`Got unexpected empty seed data during instant validation`
)
}
const childRoute = childRoutes[parallelRouteKey]
const [childSegment] = childRoute
const childPath = createChildSegmentPath(
path,
parallelRouteKey,
childSegment
)
traverseCacheNodeSegments(
childPath,
childRoute,
childSeedData,
processSegment
)
}
}Caution
If a parallel route key in childRoutes lacks corresponding parallelRoutesData within the cache node seed data, traverseCacheNodeSegments throws an InvariantError indicating unexpected empty seed data during instant validation.
Prefetch generation relies on structured definitions for root tree prefetches, segment parameters, and response payloads. The SegmentPrefetchResponse format packs a build ID alongside an array of nullable segment prefetches representing terminal segments and inlined ancestors.
The client-side hydration lifecycle bridges the server-emitted Flight stream with the browser DOM via specialized segment callbacks and stream registry writers. When the browser initializes app routing, self.__next_f captures inlined flight segments and decodes them into a React-readable stream.
function nextServerDataCallback(seg: FlightSegment): void {
if (seg[0] === 0) {
initialServerDataBuffer = []
} else if (seg[0] === 1) {
if (!initialServerDataBuffer)
throw new Error('Unexpected server data: missing bootstrap script.')
if (initialServerDataWriter) {
initialServerDataWriter.enqueue(encoder.encode(seg[1]))
} else {
initialServerDataBuffer.push(seg[1])
}
} else if (seg[0] === 2) {
initialFormStateData = seg[1]
} else if (seg[0] === 3) {
if (!initialServerDataBuffer)
throw new Error('Unexpected server data: missing bootstrap script.')
const binaryString = atob(seg[1])
const decodedChunk = new Uint8Array(binaryString.length)
for (var i = 0; i < binaryString.length; i++) {
decodedChunk[i] = binaryString.charCodeAt(i)
}
if (initialServerDataWriter) {
initialServerDataWriter.enqueue(decodedChunk)
} else {
initialServerDataBuffer.push(decodedChunk)
}
}
}Warning
If a segment with type 1 or 3 arrives before a bootstrap segment of type 0 has initialized initialServerDataBuffer, nextServerDataCallback throws an error stating that the server data is missing the bootstrap script.
Flight segments emitted into self.__next_f use prefix tags to differentiate bootstrap markers, text chunks, form state payloads, and base64-encoded binary chunks.
Client initialization coordinates reader streams, client component loading instrumentation, and DOM content loaded states to complete React hydration.
export async function hydrate(
instrumentationHooks: ClientInstrumentationHooks | null,
assetPrefix: string
) {
let staticIndicatorState: StaticIndicatorState | undefined
let webSocket: WebSocket | undefined
if (process.env.__NEXT_DEV_SERVER) {
const { createWebSocket } =
require('./dev/hot-reloader/app/web-socket') as typeof import('./dev/hot-reloader/app/web-socket')
staticIndicatorState = { pathname: null, appIsrManifest: null }
webSocket = createWebSocket(assetPrefix, staticIndicatorState)
}
const initialRSCPayload = await initialServerResponse
if (process.env.__NEXT_USE_OFFLINE) {
require('./components/offline') as typeof import('./components/offline')
}
if (initialRSCPayload.b) {
setNavigationBuildId(initialRSCPayload.b!)
} else {
setNavigationBuildId(getDeploymentId()!)
}
const initialTimestamp = Date.now()
const actionQueue: AppRouterActionQueue = createMutableActionQueue(
createInitialRouterState({
navigatedAt: initialTimestamp,
initialRSCPayload,
initialFlightStreamForCache,
location: window.location,
}),
instrumentationHooks
)
const reactEl = (
<StrictModeIfEnabled>
<HeadManagerContext.Provider value={{ appDir: true }}>
<Root>
<ServerRoot
initialRSCPayload={initialRSCPayload}
actionQueue={actionQueue}
webSocket={webSocket}
staticIndicatorState={staticIndicatorState}
/>
</Root>
</HeadManagerContext.Provider>
</StrictModeIfEnabled>
)
if (document.documentElement.id === '__next_error__') {
let element = reactEl
if (process.env.NODE_ENV !== 'production') {
const { RootLevelDevOverlayElement } =
require('../next-devtools/userspace/app/client-entry') as typeof import('../next-devtools/userspace/app/client-entry')
element = (
<RootLevelDevOverlayElement>{element}</RootLevelDevOverlayElement>
)
}
ReactDOMClient.createRoot(appElement, reactRootOptions).render(element)
} else {
React.startTransition(() => {
ReactDOMClient.hydrateRoot(appElement, reactEl, {
...reactRootOptions,
formState: initialFormStateData,
})
})
}
if (process.env.__NEXT_DEV_SERVER) {
const { linkGc } =
require('./app-link-gc') as typeof import('./app-link-gc')
linkGc()
}
}