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:
Staged Dynamic Rendering is an advanced rendering architecture in Next.js that structures server-side rendering into discrete lifecycle stages—ranging from early static shell generation through runtime phases to fully dynamic rendering—controlled by the StagedRenderingController. This model allows Next.js to isolate static components and render cacheable data without deopting an entire React tree, replacing coarse deopts with fine-grained stage progression. By coordinating triggers, tracking dynamic data access, and orchestrating delayed parameter resolutions, the system prevents unnecessary blocking while managing cache interactions and serializing Flight streams effectively.
Sources: packages/next/src/server/app-render/app-render.tsx:944-952, packages/next/src/server/app-render/staged-rendering.ts:4-20, packages/next/src/server/app-render/dynamic-rendering.ts:6-10
The staged rendering lifecycle divides server rendering into sequential, controllable execution steps managed by the StagedRenderingController class in staged-rendering.ts. Progression through these stages controls when specific data kinds—such as session data, static link data, and runtime link data—become available to the React rendering tree. The controller maintains an internal map of triggers for every advanceable render stage, resolving pending promises as the render moves forward.
Sources: packages/next/src/server/app-render/staged-rendering.ts:4-20, packages/next/src/server/app-render/staged-rendering.ts:84-106
The progression is governed by RENDER_STAGE_ADVANCE_ORDER, which sequences static and runtime phases into distinct shell, early, and standard milestones before reaching the final dynamic stage.
The StagedRenderingController constructor initializes stage triggers and wires up event listeners for abort signals and abandon controllers. When advanceStage(targetStage) is called, the controller verifies that the target does not exceed finalStage and checks whether the target is ahead of currentStage.
Sources: packages/next/src/server/app-render/staged-rendering.ts:108-148, packages/next/src/server/app-render/staged-rendering.ts:314-325
The stage advancement flow executes through the following call chain:
StagedRenderingController.advanceStage() → determines index range via RENDER_STAGE_ADVANCE_ORDER.indexOf() → iterates over intermediate stages calling this.resolveStage() → fireStageTrigger() → executes registered listeners in trigger._listeners and invokes trigger._resolvePromise().
// Example instantiation and stage progression sequence in node rendering
const stageController = new StagedRenderingController({
abortSignal: null,
abandonController: null,
shouldTrackSyncIO: false,
finalStage: null,
})
// Advance through static stages during node flight stream generation
stageController.advanceStage(RenderStage.ShellStatic)
stageController.advanceStage(RenderStage.Static)
stageController.advanceStage(RenderStage.Dynamic)Sources: packages/next/src/server/app-render/app-render.tsx:944-952, packages/next/src/server/app-render/app-render.tsx:1035-1073, packages/next/src/server/app-render/staged-rendering.ts:314-362, packages/next/src/server/app-render/staged-rendering.ts:440-466
Note
cancelStageTrigger suppresses unhandled rejection warnings automatically by attaching a no-op catch handler to trigger.promise when an abort signal rejects pending stage triggers.
Sources: packages/next/src/server/app-render/staged-rendering.ts:124-137, packages/next/src/server/app-render/staged-rendering.ts:469-482
Dynamic data access and postponement handling govern how Next.js tracks runtime variables, deopts component trees, and coordinates client-side navigation hooks. When code reads dynamic properties or navigation parameters during prerendering, Next.js captures these occurrences via explicit tracking structures, triggers React postponements, or aborts static generation depending on the active render unit and configuration flags.
Sources: packages/next/src/server/app-render/dynamic-rendering.ts:1-21, packages/next/src/server/app-render/dynamic-rendering.ts:167-236
The DynamicTrackingState structure manages properties recorded during Server Component rendering. It maintains isDebugDynamicAccesses, an array of dynamicAccesses storing individual DynamicAccess objects containing an optional stack trace and the accessed expression string, alongside sync error handling flags syncDynamicErrorWithStack and syncDynamicErrorWithStackPostMicrotask.
The createDynamicTrackingState(isDebugDynamicAccesses) function instantiates this tracking object with an empty dynamicAccesses array and null error states. When a dynamic scope is entered, annotateDynamicAccess(expression, prerenderStore) pushes a new entry into dynamicAccesses, capturing new Error().stack if isDebugDynamicAccesses is enabled.
Sources: packages/next/src/server/app-render/dynamic-rendering.ts:124-133, packages/next/src/server/app-render/dynamic-rendering.ts:612-625
Sources: packages/next/src/server/app-render/dynamic-rendering.ts:84-133, packages/next/src/server/app-render/dynamic-rendering.ts:612-625
Postponed state representations handle data and HTML segments that suspend during partial prerendering (PPR). The DynamicState enum distinguishes between RSC render data (DATA = 1) and HTML shell render phases (HTML = 2).
The parsePostponedState(state, interpolatedParams, maxPostponedStateSizeBytes) function parses a serialized postponed state string by extracting the initial length match, slicing the postponed string payload and resume data cache, and replacing fallback route parameters using getDynamicParam when interpolated params are provided.
Warning
If parsing fails due to malformed string prefixes or JSON errors, parsePostponedState catches the exception, logs it, and falls back to a default DynamicDataPostponedState instance rather than crashing the request parser.
Client hooks such as usePathname, useSearchParams, useParams, useSelectedLayoutSegments, and useSelectedLayoutSegment invoke useDynamicRouteParams or useDynamicSearchParams during server-side rendering to signal runtime dependency.
Sources: packages/next/src/client/components/navigation.ts:65-66, packages/next/src/client/components/navigation.ts:122-123, packages/next/src/client/components/navigation.ts:225-226, packages/next/src/client/components/navigation.ts:277-280, packages/next/src/client/components/navigation.ts:332-335
The dynamic hook invocation trace flows through the following call chain:
useSelectedLayoutSegment() calls useSelectedLayoutSegments(parallelRouteKey) — accesses layout context and retrieves parallel segment paths.useSelectedLayoutSegments() invokes useDynamicRouteParams('useSelectedLayoutSegments()') — checks store types and manages cache components fallback parameters.useDynamicRouteParams() evaluates work unit stores (prerender-client) and uses makeClientHookHangingPromise to suspend rendering when fallback parameters are present.Sources: packages/next/src/server/app-render/dynamic-rendering.ts:627-642, packages/next/src/client/components/navigation.ts:332-337
Sources: packages/next/src/server/app-render/dynamic-rendering.ts:627-642, packages/next/src/client/components/navigation.ts:332-337
Sources: packages/next/src/server/app-render/dynamic-rendering.ts:627-642, packages/next/src/server/app-render/postponed-state.ts:15-25, packages/next/src/server/app-render/postponed-state.ts:168-190, packages/next/src/server/dynamic-rendering-utils.ts:108-113
Parameter resolution within staged rendering bridges static shells and dynamic server execution. Route parameters (params) and search parameters (searchParams) are accessed via async promises that leverage staged progression controls to delay unblocking until specific render boundaries or segment stages are reached.
Sources: packages/next/src/server/request/params.ts:331-380, packages/next/src/server/app-render/vary-params.ts:21-35
The lifecycle of parameter resolution coordinates execution across static and dynamic boundaries. When server routes evaluate parameters during prerendering, createServerParamsForRoute determines the appropriate execution path based on the active WorkUnitStore type.
The call-chain execution walkthrough for parameter resolution proceeds as follows:
createServerParamsForRoute retrieves the current workUnitStore and dispatches static route params to createStaticPrerenderParams.
Sources: packages/next/src/server/request/params.ts:138-158createStaticPrerenderParams inspects whether __NEXT_APP_SHELLS is active and invokes stagedRendering.delayUntilStage with the late static link data stage (RenderStage.Static).
Sources: packages/next/src/server/request/params.ts:359-380, packages/next/src/server/dynamic-rendering-utils.ts:198-199delayUntilStage obtains the underlying stage promise by invoking this.getStagePromise(stage).
Sources: packages/next/src/server/app-render/staged-rendering.ts:372-377getStagePromise returns this.triggers[stage].promise, which remains pending until the staged rendering controller advances past that specific milestone.
Sources: packages/next/src/server/app-render/staged-rendering.ts:364-366Sources: packages/next/src/server/request/params.ts:138-158, packages/next/src/server/request/params.ts:359-380, packages/next/src/server/app-render/staged-rendering.ts:364-377
Note
Even when parameters are entirely static, they are intentionally excluded from the initial HTML shell by delaying their resolution until the static stage is reached.
To support granular segment caching and flight serialization, Next.js tracks which parameter keys are accessed during rendering using VaryParamsAccumulator structures.
Sources: packages/next/src/server/app-render/vary-params.ts:21-105, packages/next/src/server/app-render/vary-params.ts:244-298
Tip
When optional catch-all parameters are present (...slug), createVaryingParams employs a JavaScript Proxy to intercept get, has, and ownKeys traps, ensuring missing properties and enumerations correctly register as varying accesses.
Sources: packages/next/src/server/app-render/vary-params.ts:71-91, packages/next/src/server/app-render/vary-params.ts:244-298
Next.js intercepts synchronous platform operations (such as reads of current time, random number generators, or cryptographic functions) during pre-rendering to prevent non-deterministic values from being baked into static outputs. When a synchronous I/O action occurs within a pre-render or staged execution context, platform extensions capture the access, format specific diagnostic messages, and trigger stage interruptions.
Sources: packages/next/src/server/node-environment-extensions/io-utils.tsx:13-103, packages/next/src/server/app-render/sync-io-messages.ts:33-85
When code executes inside a Node environment extension, io() intercepts calls using a three-argument signature: io(expression: string, type: SyncIOApiType). The execution flows through work-unit storage checks and controller evaluation.
Sources: packages/next/src/server/node-environment-extensions/io-utils.tsx:13-103, packages/next/src/server/app-render/staged-rendering.ts:154-185
The detailed call chain proceeds as follows:
io(expression, type) retrieves workUnitAsyncStorage and workAsyncStorage. Sources: packages/next/src/server/node-environment-extensions/io-utils.tsx:13-15workUnitStore.type, if it matches 'prerender', 'prerender-runtime', or 'prerender-client', it checks whether prerenderSignal.aborted is false. Sources: packages/next/src/server/node-environment-extensions/io-utils.tsx:21-54abortOnSynchronousPlatformIOAccess(...), which records the stack via applyOwnerStack(createSyncIOError(...)) into dynamicTracking.syncDynamicErrorWithStack and invokes prerenderStore.controller.abort(error). Sources: packages/next/src/server/app-render/dynamic-rendering.ts:323-342, packages/next/src/server/node-environment-extensions/io-utils.tsx:29-34workUnitStore.type is 'request', it inspects stageController.shouldTrackSyncInterrupt(). Sources: packages/next/src/server/node-environment-extensions/io-utils.tsx:55-57createSyncIOError or createSyncIORuntimeError based on whether the current stage is Static/EarlyStatic or Runtime, wraps it with applyOwnerStack(), and invokes stageController.syncInterruptCurrentStageWithReason(syncIOError). Sources: packages/next/src/server/node-environment-extensions/io-utils.tsx:58-77Caution
During EarlyRuntime stages, synchronous I/O throws an error because the segment is runtime-prefetchable and an interruption would abort the prefetch prematurely, whereas Runtime stages permit synchronous I/O since non-prefetchable segments will never be runtime prefetched.
Synchronous I/O tracking categorizes operations into specific API types, mapping each to dedicated documentation URLs and remediation guidance.
Sources: packages/next/src/server/app-render/sync-io-messages.ts:1-19, packages/next/src/server/app-render/sync-io-messages.ts:21-46
Note
Server-side sync I/O errors recommend fixing the issue by adding a dynamic data access like await connection(), caching the value with "use cache", or moving rendering to a Client Component. Client-side sync I/O errors suggest wrapping in <Suspense> or moving the read into a useEffect or event handler.
Cache coordination and route stream generation unify use-cache semantics, resume data cache serialization, and Flight stream generation. When handling cached or dynamic data dependencies within Next.js application render workflows, cache entries are evaluated for expiration, stale times, and dynamic omission before being embedded into the React Server Components (RSC) payload or staged execution pipelines. Sources: packages/next/src/server/use-cache/use-cache-wrapper.ts:2837-2849, packages/next/src/server/app-render/app-render.tsx:914-964
During static generation or prerendering, cache entries with a revalidate value of 0 or an expiration time under the dynamic expiration threshold are omitted from the static shell. This creates a dynamic hole filled during resume operations. Sources: packages/next/src/server/use-cache/use-cache-wrapper.ts:2851-2892
Tip
When a cache entry's revalidate is set to 0 or its expiration falls below DYNAMIC_EXPIRE, the system avoids generating static pages for such data, replacing them with hanging promises that resolve via resume data caches.
When production staged dynamic flight renders execute in Node.js streams, the request store initializes stale time trackers, stage controllers, vary params accumulators, and async API promises. If runtime prefetching is enabled via loader trees, a prerender resume data cache and cache signal are spawned. Sources: packages/next/src/server/app-render/app-render.tsx:914-964
The staged flight render follows a precise sequential task execution pipeline using runInSequentialTasks:
stageController.advanceStage(RenderStage.ShellStatic) advances the stage. Sources: packages/next/src/server/app-render/app-render.tsx:1032-1036workUnitAsyncStorage.run(requestStore, renderToNodeFlightStream, ...) generates the source node flight stream. Sources: packages/next/src/server/app-render/app-render.tsx:1037-1044new ReplayableNodeStream(sourceStream) creates replay streams for dynamic and static outputs, counting shell and static stage bytes. Sources: packages/next/src/server/app-render/app-render.tsx:1046-1055stageController.advanceStage(RenderStage.Static) moves execution into the static stage. Sources: packages/next/src/server/app-render/app-render.tsx:1059-1061staleTimeIterable.close() and finishAccumulatingVaryParams(requestStore.varyParamsAccumulator) flush tracking data. Sources: packages/next/src/server/app-render/app-render.tsx:1062-1070stageController.advanceStage(RenderStage.Dynamic) completes the progression into the dynamic stage. Sources: packages/next/src/server/app-render/app-render.tsx:1071-1074Warning
Postponed states and resume data caches serialize into string formats incorporating payload length identifiers. If fallback route params are present, replacements are serialized and prepended to ensure parameter interpolation during resumption.