Relevant source files
The following files were used as context for generating this wiki page:
- packages/next/src/server/app-render/dynamic-rendering.ts
- packages/next/src/build/index.ts
- packages/next/src/build/templates/app-page-runtime.ts
- packages/next/src/server/dynamic-rendering-utils.ts
- packages/next/src/server/base-server.ts
- packages/next/src/build/adapter/build-complete.ts
- packages/next/src/server/config-shared.ts
- packages/next/src/server/route-modules/app-route/module.ts
- packages/next/src/client/components/render-tree.ts
- packages/next/src/server/app-render/dynamic-rendering-client.ts
- packages/next/src/server/request/search-params.ts
- packages/next/src/server/request/draft-mode.ts
- packages/next/src/server/web/spec-extension/unstable-no-store.ts
- packages/next/src/client/components/segment-cache/scheduler.ts
- packages/next/src/server/render.tsx
- packages/next/src/server/app-render/vary-params.ts
- packages/next/src/server/request/params.ts
- packages/next/src/server/request/fallback-params.ts
- packages/next/src/server/app-render/dynamic-access-async-storage.external.ts
- packages/next/src/server/app-render/postponed-state.ts
- packages/next/src/server/resume-data-cache/resume-data-cache.ts
Overview
Dynamic Rendering encompasses the mechanisms Next.js uses to track runtime data access, manage partial prerendering (PPR) shells, and defer or evaluate dynamic route segments, search parameters, and draft mode states. By intercepting dynamic operations through Async Storage execution contexts, the system coordinates static generation bailouts, generates artificial hanging promises, and accumulates varying parameters to construct and restore suspended streams in the App Rendering pipeline.
At build time in the Build Pipeline, route entry configurations determine whether pages are fully static, dynamically rendered on demand, or split into static prerender shells with postponed dynamic holes. The Server Architecture consumes these manifests to govern cache keys, evaluate prefetch headers, and control stream output during live requests.
Sources: packages/next/src/server/app-render/dynamic-rendering.ts:1282-1293, packages/next/src/server/dynamic-rendering-utils.ts:342-351, packages/next/src/server/app-render/postponed-state.ts:84-87
Dynamic Tracking Mechanics
Validation and Bailout Enforcement
The server evaluates execution paths using validation states and store checks to trigger static generation bailouts or runtime errors when disallowed dynamic access occurs. Functions such as throwIfDisallowedDynamic and throwIfDisallowedDynamicInStaticRoute inspect prelude states, dynamic error collections, and viewport flags to determine whether a route can produce a static shell or must terminate with a StaticGenBailoutError.
export function throwIfDisallowedDynamicInStaticRoute(
workStore: WorkStore,
prelude: PreludeState,
dynamicValidation: DynamicValidationState,
serverDynamic: DynamicTrackingState,
isFallbackShell: boolean,
allowEmptyStaticShell: boolean,
isServerPartial: boolean
): void {
const reasons = getDisallowedReasonsInStaticRoute(
workStore,
prelude,
dynamicValidation,
serverDynamic,
isFallbackShell,
allowEmptyStaticShell,
isServerPartial
)
if (reasons.length > 0) {
for (const reason of reasons) {
logDisallowedDynamicError(workStore, reason)
}
throw new StaticGenBailoutError()
}
}Sources: packages/next/src/server/app-render/dynamic-rendering.ts:1366-1390
Warning
If a route produces an empty prelude (PreludeState.Empty) without recorded dynamic errors during validation, Next.js treats the condition as an internal tracking failure and throws an InvariantError rather than a standard bailout.
Sources: packages/next/src/server/app-render/dynamic-rendering.ts:1437-1447
Declarative Opt-Outs and Abort Controllers
Declarative opt-outs via unstable_noStore inspect active work stores and evaluate work unit store types to mark scopes as dynamic, though behavior varies depending on whether the execution context operates under dynamic I/O or standard rendering modes.
export function unstable_noStore() {
const callingExpression = 'unstable_noStore()'
const store = workAsyncStorage.getStore()
const workUnitStore = workUnitAsyncStorage.getStore()
if (!store) {
// This generally implies we are being called in Pages router. We should probably not support
// unstable_noStore in contexts outside of `react-server` condition but since we historically
// have not errored here previously, we maintain that behavior for now.
return
} else if (store.forceStatic) {
return
} else {
store.isUnstableNoStore = true
if (workUnitStore) {
switch (workUnitStore.type) {
case 'prerender':
case 'prerender-client':
case 'validation-client':
case 'prerender-runtime':
// unstable_noStore() is a noop in Dynamic I/O.
return
case 'prerender-legacy':
case 'request':
case 'cache':
case 'private-cache':
case 'unstable-cache':
case 'build-time-generator':
break
default:
workUnitStore satisfies never
}
}
markCurrentScopeAsDynamic(store, workUnitStore, callingExpression)
}
}Sources: packages/next/src/server/web/spec-extension/unstable-no-store.ts:20-54
| Work Unit Store Type | Behavior in unstable_noStore() |
|---|---|
prerender, prerender-client, validation-client, prerender-runtime | No-op under Dynamic I/O |
prerender-legacy, request, cache, private-cache, unstable-cache, build-time-generator | Marks current scope as dynamic |
Sources: packages/next/src/server/web/spec-extension/unstable-no-store.ts:33-47
Additionally, abortOnDynamicAccess interacts with dynamicAccessAsyncStorage to abort ongoing fetches or cache reads when runtime or fallback parameter dependencies are detected.
export function abortOnDynamicAccess(
reason: DynamicAccessReason,
error: Error
) {
const store = dynamicAccessAsyncStorageInstance.getStore()
if (store !== undefined) {
// A cache may read both kinds of data before cancellation finishes. Only
// fallback-only dependencies can become static when the params are known.
if (store.reason !== 'runtime') {
store.reason = reason
}
store.abortController.abort(error)
}
}Sources: packages/next/src/server/app-render/dynamic-access-async-storage.external.ts:16-29
Hanging Promises Implementation
Artificial Unresolved Promises and Staged Tracking
When Next.js encounters dynamic hooks or runtime data access during pre-rendering or static generation stages, it creates artificial unresolved promises to suspend rendering until dynamic stages or client hydration occur. Functions such as makeHangingPromise and makeUnknownRuntimeDataHangingPromise construct promises tied to an abort signal and an error expression, optionally tracking URL data or session data access via workUnitStore.
expression: string,
workUnitStore: WorkUnitStore | null
): Promise<T> {
const promise = makeHangingPromiseWithError<T>(
signal,
new HangingPromiseRejectionError(route, expression)
)
if (workUnitStore === null) {
return promise
}
return trackPromiseUsed(
promise,
trackURLDataAccessed.bind(null, workUnitStore, expression)
)
}Sources: packages/next/src/server/dynamic-rendering-utils.ts:168-182
Runtime data tracking relies on PrerenderDataKind constants to classify whether accessed data affects session storage, URL parameters, or fallback parameters. The internal trackRuntimeDataAccessed function inspects the active workUnitStore stage to determine whether to deopt the shell, the prefetch, or both.
const enum PrerenderDataKind {
SessionData = 1,
UrlData = 2,
FallbackParams = 3,
}Sources: packages/next/src/server/dynamic-rendering-utils.ts:353-357
| Prerender Data Kind Value | Constant Name | Target Deopt Scope |
|---|---|---|
1 | SessionData | Shell and Prefetch |
2 | UrlData | Prefetch only |
3 | FallbackParams | Prefetch or transient upgradeable fallback |
Sources: packages/next/src/server/dynamic-rendering-utils.ts:354-356, packages/next/src/server/dynamic-rendering-utils.ts:402-485
Note
Accesses occurring after RenderStage.NavigationStatic are ignored because the purpose of runtime tracking is to evaluate whether a runtime prefetch provides more complete data than a static prefetch.
Sources: packages/next/src/server/dynamic-rendering-utils.ts:379-387
Client Hook Hanging Promises and Work Unit Validation
On the client-side app rendering and routing context, functions like useDynamicRouteParams and useDynamicSearchParams evaluate active work unit store types to suspend component execution. When running inside a prerender-client context with cache component semantics, these hooks invoke React.use() with a hanging promise created via makeClientHookHangingPromise.
export function useDynamicRouteParams(expression: string) {
const workStore = workAsyncStorage.getStore()
const workUnitStore = workUnitAsyncStorage.getStore()
if (workStore && workUnitStore) {
switch (workUnitStore.type) {
case 'prerender-client': {
const fallbackParams = workUnitStore.fallbackRouteParams
if (fallbackParams && fallbackParams.size > 0) {
// We are in a prerender with cacheComponents semantics. We are going to
// hang here and never resolve. This will cause the currently
// rendering component to effectively be a dynamic hole.
React.use(
makeClientHookHangingPromise(
workUnitStore.renderSignal,
new ClientHookDynamicError(workStore.route, expression)
)
)
}
breakSources: packages/next/src/server/app-render/dynamic-rendering-client.ts:27-46
| Work Unit Store Type | useDynamicRouteParams Behavior | useDynamicSearchParams Behavior |
|---|---|---|
prerender-client | Suspends via hanging promise if fallback parameters exist | Suspends via hanging promise |
prerender / prerender-runtime | Throws InvariantError | Throws InvariantError |
validation-client | Bypasses fallback route parameter checks | Bypasses hanging promise for validation |
cache / private-cache / unstable-cache | Throws InvariantError | Throws InvariantError |
build-time-generator | Throws InvariantError mentioning function name | Throws InvariantError mentioning function name |
request | No-op | No-op |
Sources: packages/next/src/server/app-render/dynamic-rendering-client.ts:31-73, packages/next/src/server/app-render/dynamic-rendering-client.ts:93-135
Warning
Calling search parameter or dynamic route parameter hooks from a server component during standard server prerendering triggers an InvariantError, as Next.js expects these expressions to be intercepted before server component inclusion.
Sources: packages/next/src/server/app-render/dynamic-rendering-client.ts:48-51, packages/next/src/server/app-render/dynamic-rendering-client.ts:119-123
Fallback Parameters Resolution
Staged Fallback Parameters and Segment Placeholders
export type OpaqueFallbackRouteParams = ReadonlyMap<string, string>
/**
* Creates an opaque fallback route params object from the fallback route params.
*
* @param fallbackRouteParams the fallback route params
* @returns the opaque fallback route params
*/
export function createOpaqueFallbackRouteParams(
fallbackRouteParams: readonly FallbackRouteParam[]
): OpaqueFallbackRouteParams | null {
// If there are no fallback route params, we can return early.
if (fallbackRouteParams.length === 0) return null
// As we're creating unique keys for each of the dynamic route params, we only
// need to generate a unique ID once per request because each of the keys will
// be also be unique.
const uniqueID = Math.random().toString(16).slice(2)
const keys = new Map<string, string>()
// Generate a unique key for the fallback route param, if this key is found
// in the static output, it represents a bug in cache components.
for (const { paramName } of fallbackRouteParams) {
keys.set(paramName, `%%drp:${paramName}:${uniqueID}%%`)
}
return keys
}Sources: packages/next/src/server/request/fallback-params.ts:13-41
Note
Opaque fallback route parameters map unknown parameter names to unique randomized placeholder identifiers (%%drp:${paramName}:${uniqueID}%%). These placeholders prevent unprerendered dynamic segments from leaking into rendered UI during static shell generation.
Sources: packages/next/src/server/request/fallback-params.ts:9-41
export function getStagedFallbackParams(route: {
fallbackRouteParams: readonly FallbackRouteParam[] | undefined
remainingPrerenderableParams?: readonly FallbackRouteParam[]
throwOnEmptyStaticShell?: boolean
}): OpaqueFallbackRouteParams | null {
const { fallbackRouteParams, remainingPrerenderableParams } = route
if (!fallbackRouteParams?.length) {
return null
}
if (route.throwOnEmptyStaticShell === undefined) {
throw new InvariantError(
'Expected throwOnEmptyStaticShell for a route with fallback params'
)
}
// A required shell keeps all of its fallback params deferred.
if (route.throwOnEmptyStaticShell || !remainingPrerenderableParams?.length) {
return createOpaqueFallbackRouteParams(fallbackRouteParams)
}
return createOpaqueFallbackRouteParams(
fallbackRouteParams.filter(
(param) =>
!remainingPrerenderableParams.some(
(prerenderableParam) =>
prerenderableParam.paramName === param.paramName
)
)
)
}Sources: packages/next/src/server/request/fallback-params.ts:56-86
| Helper Function Name | Input Signature | Purpose / Behavior |
|---|---|---|
createOpaqueFallbackRouteParams | fallbackRouteParams: readonly FallbackRouteParam[] | Generates a randomized OpaqueFallbackRouteParams map assigning unique wrapper strings to each parameter name. |
getStagedFallbackParams | route: { fallbackRouteParams, remainingPrerenderableParams?, throwOnEmptyStaticShell? } | Selects which fallback parameters are deferred for a given target shell based on build metadata and remaining prerenderable parameters. |
buildDynamicSegmentPlaceholder | param: Pick<FallbackRouteParam, 'paramName' | 'paramType'> | Constructs bracketed dynamic segment expressions based on parameter repeat and optional properties. |
getPlaceholderFallbackRouteParams | params: Record<string, any> | undefined, fallbackRouteParams: readonly FallbackRouteParam[] | Filters fallback parameters whose active values match generated dynamic segment placeholders. |
getFallbackRouteParams | page: string, routeModule: AppPageRouteModule | Parses the normalized route, extracts pathname segments from the loader tree, and resolves unknown parameters. |
Sources: packages/next/src/server/request/fallback-params.ts:21-165
Important
When throwOnEmptyStaticShell is true or no remainingPrerenderableParams exist, getStagedFallbackParams defers all fallback parameters because the required shell requires a non-empty prelude matching the exact validated shape.
Sources: packages/next/src/server/request/fallback-params.ts:72-75
Draft Mode Tracking
Work Unit Scopes and Execution Contexts
switch (workUnitStore.type) {
case 'prerender-runtime': {
// TODO(runtime-ppr): does it make sense to delay this? normally it's always microtasky
const { stagedRendering } = workUnitStore
if (stagedRendering) {
return stagedRendering.delayUntilStage(
RENDER_STAGES_BY_DATA_KIND.sessionData,
'draftMode',
new DraftMode(workUnitStore.draftMode)
)
} else {
return createOrGetCachedDraftMode(workUnitStore.draftMode, workStore)
}
}
case 'request':
return createOrGetCachedDraftMode(workUnitStore.draftMode, workStore)
case 'cache':
case 'private-cache':
case 'unstable-cache':
// Inside of `"use cache"` or `unstable_cache`, draft mode is available if
// the outmost work unit store is a request store (or a runtime prerender),
// and if draft mode is enabled.
const draftModeProvider = getDraftModeProviderForCacheScope(
workStore,
workUnitStore
)
if (draftModeProvider) {
return createOrGetCachedDraftMode(draftModeProvider, workStore)
}
// Otherwise, we fall through to providing an empty draft mode.
// eslint-disable-next-line no-fallthrough
case 'prerender':
case 'prerender-legacy':
// Return empty draft mode
return createOrGetCachedDraftMode(null, workStore)
case 'prerender-client':
case 'validation-client': {
const exportName = '`draftMode`'
throw new InvariantError(
`${exportName} must not be used within a Client Component. Next.js should be preventing ${exportName} from being included in Client Components statically, but did not in this case.`
)
}
case 'build-time-generator':
throw new Error(
`Route ${workStore.route} used \`${callingExpression}()\` inside \`${workUnitStore.functionName}\`. This is not supported because \`${workUnitStore.functionName}\` runs at build time without an HTTP request. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context`
)
default:
return workUnitStore satisfies never
}Sources: packages/next/src/server/request/draft-mode.ts:40-92
| Work Unit Type | Action / Behavior on draftMode() Access |
|---|---|
prerender-runtime | Delays until session data stage via stagedRendering.delayUntilStage, or retrieves cached draft mode. |
request | Resolves or creates cached draft mode via createOrGetCachedDraftMode. |
cache, private-cache, unstable-cache | Resolves provider via getDraftModeProviderForCacheScope if enclosed by a valid request store or runtime prerender. |
prerender, prerender-legacy | Returns an empty draft mode instance. |
prerender-client, validation-client | Throws an InvariantError indicating draftMode must not be used within Client Components. |
build-time-generator | Throws an Error because build-time execution lacks an HTTP request context. |
Sources: packages/next/src/server/request/draft-mode.ts:40-92
Caution
Invoking draftMode() within Client Components (prerender-client or validation-client) immediately throws an InvariantError, as Next.js restricts draft mode hooks to server contexts.
Sources: packages/next/src/server/request/draft-mode.ts:78-84
Mutation Tracking and Bailout Handling
function trackDynamicDraftMode(expression: string, constructorOpt: Function) {
const workStore = workAsyncStorage.getStore()
const workUnitStore = workUnitAsyncStorage.getStore()
if (workStore) {
// We have a store we want to track dynamic data access to ensure we
// don't statically generate routes that manipulate draft mode.
if (workUnitStore?.phase === 'after') {
throw new Error(
`Route ${workStore.route} used "${expression}" inside \`after()\`. The enabled status of \`draftMode()\` can be read inside \`after()\` but you cannot enable or disable \`draftMode()\`. See more info here: https://nextjs.org/docs/app/api-reference/functions/after`
)
}
if (workStore.dynamicShouldError) {
throw new StaticGenBailoutError(
`Route ${workStore.route} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`
)
}
if (workUnitStore) {
switch (workUnitStore.type) {
case 'cache':
case 'private-cache': {
const error = createDraftModeMutationInUseCacheError(
workStore.route,
expression
)
Error.captureStackTrace(error, constructorOpt)
applyOwnerStack(error)
workStore.invalidDynamicUsageError ??= error
throw error
}
case 'unstable-cache':
throw createDraftModeMutationInUnstableCacheError(
workStore.route,
expression
)
case 'prerender':
case 'prerender-runtime': {
const error = new Error(
`Route ${workStore.route} used ${expression} without first calling \`await connection()\`. See more info here: https://nextjs.org/docs/messages/next-prerender-sync-headers`
)
return abortAndThrowOnSynchronousRequestDataAccess(
workStore.route,
expression,
error,
workUnitStore
)
}
case 'prerender-client':
case 'validation-client':
const exportName = '`draftMode`'
throw new InvariantError(
`${exportName} must not be used within a Client Component. Next.js should be preventing ${exportName} from being included in Client Components statically, but did not in this case.`
)
case 'prerender-legacy':
workUnitStore.revalidate = 0
const err = new DynamicServerError(
`Route ${workStore.route} couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error`
)
workStore.dynamicUsageDescription = expression
workStore.dynamicUsageStack = err.stack
throw err
case 'request':
trackDynamicDataInDynamicRender(workUnitStore)
break
case 'build-time-generator':
throw new Error(
`Route ${workStore.route} used \`${expression}\` inside \`${workUnitStore.functionName}\`. This is not supported because \`${workUnitStore.functionName}\` runs at build time without an HTTP request. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context`
)
default:
workUnitStore satisfies never
}
}
}Sources: packages/next/src/server/request/draft-mode.ts:194-271
| Work Unit Store Type | Mutation Response (trackDynamicDraftMode) |
|---|---|
cache, private-cache | Throws a mutation error created by createDraftModeMutationInUseCacheError. |
unstable-cache | Throws a mutation error created by createDraftModeMutationInUnstableCacheError. |
prerender, prerender-runtime | Aborts and throws due to synchronous request data access without await connection(). |
prerender-legacy | Sets revalidate = 0 and throws a DynamicServerError. |
request | Tracks dynamic data access via trackDynamicDataInDynamicRender. |
build-time-generator | Throws an error prohibiting runtime API use inside build-time generator functions. |
Sources: packages/next/src/server/request/draft-mode.ts:213-267
Warning
Attempting to enable or disable draft mode inside an after() execution block triggers an error because draft mode status can only be read, not mutated, post-response.
Sources: packages/next/src/server/request/draft-mode.ts:201-205
Vary Params Accumulation
VaryParamsAccumulator Mechanics
export class VaryParamsAccumulator implements AsyncIterable<VaryParamId> {
private _resolve: ((result: IteratorResult<VaryParamId>) => void) | null =
null
private _done = false
private _buffer: VaryParamId[] = []
// The set of param names already yielded. Doubles as the dedupe guard so the
// same name is never emitted twice.
private _seen: Set<VaryParamId> = new Set()
/**
* Records that a param was accessed. Yields the name into the stream the
* first time it's seen; subsequent accesses of the same name are no-ops.
*/
add(id: VaryParamId): void {
if (this._done || this._seen.has(id)) {
return
}
this._seen.add(id)
if (this._resolve !== null) {
this._resolve({ value: id, done: false })
this._resolve = null
} else {
this._buffer.push(id)
}
}
/** Ends the iteration. Best-effort: if skipped (e.g. on a sync-I/O abort),
* the consumer simply reads the params yielded so far. */
close(): void {
if (this._done) {
return
}
this._done = true
if (this._resolve !== null) {
this._resolve({ value: undefined, done: true })
this._resolve = null
}
}
[Symbol.asyncIterator](): AsyncIterator<VaryParamId> {
return {
next: () => {
if (this._buffer.length > 0) {
return Promise.resolve({ value: this._buffer.shift()!, done: false })
}
if (this._done) {
return Promise.resolve({ value: undefined, done: true })
}
return new Promise<IteratorResult<VaryParamId>>((resolve) => {
this._resolve = resolve
})
},
}
}
}Sources: packages/next/src/server/app-render/vary-params.ts:36-90
| Method / Property | Type / Signature | Behavior |
|---|---|---|
_resolve | ((result: IteratorResult<VaryParamId>) => void) | null | Pending resolver for the async iterator. |
_done | boolean | Tracks whether accumulation has been closed. |
_buffer | VaryParamId[] | Stores parameter names yielded before the iterator pulls them. |
_seen | Set<VaryParamId> | Deduplication set ensuring each parameter name is emitted at most once. |
add() | (id: VaryParamId) => void | Records a parameter access, buffering or resolving immediately. |
close() | () => void | Ends the async iterable and resolves any waiting iterator promises. |
[Symbol.asyncIterator]() | () => AsyncIterator<VaryParamId> | Returns an async iterator consuming the buffer and future additions. |
Sources: packages/next/src/server/app-render/vary-params.ts:36-90
Note
VaryParamsAccumulator acts as an AsyncIterable<VaryParamId> serialized directly by React Flight, streaming accessed parameters incrementally so that aborted prerenders capture exactly the parameters needed for the partial response.
Sources: packages/next/src/server/app-render/vary-params.ts:13-26
Parameter Proxies and Response Accumulation
export function createVaryingParams(
accumulator: VaryParamsAccumulator,
originalParamsObject: Params,
optionalCatchAllParamName: string | null
): Params {
if (optionalCatchAllParamName !== null) {
// When there's an optional catch-all param with no value (e.g.,
// [[...slug]] at /), the param doesn't exist as a property on the params
// object. Use a Proxy to track all param access — both existing params
// and the missing optional param — including enumeration patterns like
// Object.keys(), spread, for...in, and `in` checks.
return new Proxy(originalParamsObject, {
get(target, prop, receiver) {
if (typeof prop === 'string') {
if (
prop === optionalCatchAllParamName ||
Object.prototype.hasOwnProperty.call(target, prop)
) {
accumulateVaryParam(accumulator, prop)
}
}
return Reflect.get(target, prop, receiver)
},
has(target, prop) {
if (prop === optionalCatchAllParamName) {
accumulateVaryParam(accumulator, optionalCatchAllParamName)
}
return Reflect.has(target, prop)
},
ownKeys(target) {
// Enumerating the params object means the user's code may depend on
// which params are present, so conservatively track the optional
// param as accessed.
accumulateVaryParam(accumulator, optionalCatchAllParamName)
return Reflect.ownKeys(target)
},
})
}
// When there's no optional catch-all, all params exist as properties on the
// object, so we can use defineProperty getters instead of a Proxy. This is
// faster because the engine can optimize property access on regular objects
// more aggressively than Proxy trap calls.
const underlyingParamsWithVarying: Params = {}
for (const paramName in originalParamsObject) {
Object.defineProperty(underlyingParamsWithVarying, paramName, {
get() {
accumulateVaryParam(accumulator, paramName)
return originalParamsObject[paramName]
},
enumerable: true,
})
}
return underlyingParamsWithVarying
}
export function createVaryingSearchParams(
accumulator: VaryParamsAccumulator,
originalSearchParamsObject: SearchParams
): SearchParams {
// Search params have no fixed schema, so any access — missing-key reads, `in`
// checks, or enumeration — must register as varying. A Proxy is required
// (rather than per-property getters) so that enumeration of an empty
// searchParams object still triggers a vary. All accesses bucket into the
// single search params id; the segment is keyed by the whole query string.
// TODO: Split into per-param tracking if the cache key evolves.
return new Proxy(originalSearchParamsObject, {
get(target, prop, receiver) {
if (typeof prop === 'string') {
accumulateVaryParam(accumulator, SEARCH_PARAMS_VARY_ID)
}
return Reflect.get(target, prop, receiver)
},
has(target, prop) {
if (typeof prop === 'string') {
accumulateVaryParam(accumulator, SEARCH_PARAMS_VARY_ID)
}
return Reflect.has(target, prop)
},
ownKeys(target) {Sources: packages/next/src/server/app-render/vary-params.ts:205-289
| Function | Parameters | Return Type | Description |
|---|---|---|---|
createResponseVaryParamsAccumulator | None | ResponseVaryParamsAccumulator | Creates top-level head, rootParams, and a segments set. |
createVaryParamsAccumulator | None | VaryParamsAccumulator | null | Allocates and registers a new segment accumulator in the current work unit. |
createVaryingParams | accumulator, originalParamsObject, optionalCatchAllParamName | Params | Returns a Proxy for optional catch-alls or defineProperty getters for standard params. |
createVaryingSearchParams | accumulator, originalSearchParamsObject | SearchParams | Wraps search parameters in a Proxy recording SEARCH_PARAMS_VARY_ID. |
finishAccumulatingVaryParams | responseAccumulator | void | Closes head, rootParams, and all route segment accumulators. |
Sources: packages/next/src/server/app-render/vary-params.ts:123-157, packages/next/src/server/app-render/vary-params.ts:205-289, packages/next/src/server/app-render/vary-params.ts:305-313
Tip
When no optional catch-all parameter exists, createVaryingParams uses Object.defineProperty property getters rather than a Proxy, allowing JavaScript engines to optimize property access more aggressively.
Sources: packages/next/src/server/app-render/vary-params.ts:244-258
Postponed State Serialization
State Layout and Serialization Workflow
Sources: packages/next/src/server/app-render/postponed-state.ts:88-164
| Function | Parameters | Return Type | Description |
|---|---|---|---|
serializePostponedState | postponedString, resumeDataCache, isCacheComponentsEnabled, maxPostponedStateSizeBytes, disableResumeDataCacheCompression | Promise<string> | Prefixes postponed payload with length, serializes and optionally compresses resume data cache via zlib. |
getDynamicHTMLPostponedState | postponed, preludeState, fallbackRouteParams, resumeDataCache, isCacheComponentsEnabled, maxPostponedStateSizeBytes, disableResumeDataCacheCompression | Promise<string> | Encodes React postponed state, prelude marker state, and fallback route parameters into a string. |
getDynamicDataPostponedState | resumeDataCache, isCacheComponentsEnabled, maxPostponedStateSizeBytes, disableResumeDataCacheCompression, fallbackRouteParams | Promise<string> | Generates postponed state for dynamic data access without React HTML-resume requirements. |
parsePostponedState | state, maxPostponedStateSizeBytes, disableResumeDataCacheCompression | PostponedState | Parses serialized postponed string back into DynamicDataPostponedState or DynamicHTMLPostponedState. |
parseResumeDataCacheFromPostponedState | state, maxPostponedStateSizeBytes, disableResumeDataCacheCompression | RenderResumeDataCache | Extracts and parses the RenderResumeDataCache portion from a postponed state string. |
isEmptyHTMLPrelude | state | boolean | Quickly checks whether the static shell produced zero bytes before encountering a dynamic hole. |
Sources: packages/next/src/server/app-render/postponed-state.ts:88-192, packages/next/src/server/app-render/postponed-state.ts:225-327, packages/next/src/server/app-render/postponed-state.ts:392-428
Note
Next.js restricts runtime execution environment context by enforcing that stringifyResumeDataCache, deflateResumeDataCache, and createRenderResumeDataCache throw an InvariantError if invoked inside the edge runtime.
Sources: packages/next/src/server/resume-data-cache/resume-data-cache.ts:145-149, packages/next/src/server/resume-data-cache/resume-data-cache.ts:181-185, packages/next/src/server/resume-data-cache/resume-data-cache.ts:253-257
Resume Data Cache Structures and Decompression Limits
export interface RenderResumeDataCache {
/**
* Discriminator. `false` means this cache is read-only and cannot be filled
* with new entries. Used by `ResumeDataCache` consumers to narrow the union
* via standard discriminated-union narrowing (e.g. `if
* (resumeDataCache.mutable)`).
*/
readonly mutable: false
/**
* A read-only Map store for values cached by the 'use cache' React hook.
* The 'set' operation is omitted to enforce immutability.
*/
readonly cache: Omit<UseCacheCacheStore, 'set'>
/**
* A read-only Map store for cached fetch responses.
* The 'set' operation is omitted to enforce immutability.
*/
readonly fetch: Omit<FetchCacheStore, 'set'>
/**
* A read-only Map store for encrypted bound args of inline server functions.
* The 'set' operation is omitted to enforce immutability.
*/
readonly encryptedBoundArgs: Omit<EncryptedBoundArgsCacheStore, 'set'>
/**
* A read-only Map store for decrypted bound args of inline server functions.
* This is only intended for in-memory usage during pre-rendering, and must
* not be persisted in the resume store. The 'set' operation is omitted to
* enforce immutability.
*/
readonly decryptedBoundArgs: Omit<DecryptedBoundArgsCacheStore, 'set'>
/**
* A read-only in-memory Map store for rendered `ImageResponse` array buffers.
* This is only intended for in-memory usage during pre-rendering, and must
* not be persisted in the resume store. The 'set' operation is omitted to
* enforce immutability.
*/
readonly imageResponses: Omit<ImageResponseCacheStore, 'set'>
}
/**
* A mutable version of the resume data cache used during pre-rendering.
* This cache allows both reading and writing of cached values.
*/
export interface PrerenderResumeDataCache {
/**
* Discriminator. `true` means this cache is mutable and can be filled with
* new entries during this prerender. Used by `ResumeDataCache` consumers to
* narrow the union via standard discriminated-union narrowing (e.g.
* `if (resumeDataCache.mutable)`).
*/
readonly mutable: true
/**
* A mutable Map store for values cached by the 'use cache' React hook.
* Supports both 'get' and 'set' operations to build the cache during
* pre-rendering.
*/
readonly cache: UseCacheCacheStore
/**
* A mutable Map store for cached fetch responses.
* Supports both 'get' and 'set' operations to build the cache during
* pre-rendering.
*/
readonly fetch: FetchCacheStore
/**
* A mutable Map store for encrypted bound args of inline server functions.
* Supports both 'get' and 'set' operations to build the cache during
* pre-rendering.
*/
readonly encryptedBoundArgs: EncryptedBoundArgsCacheStore
/**
* A mutable Map store for decrypted bound args of inline server functions.Sources: packages/next/src/server/resume-data-cache/resume-data-cache.ts:17-116
Warning
When decompressing a persisted resume data cache using createRenderResumeDataCache, the maximum decompressed size defaults to 500 * 1024 * 1024 bytes (or 5 times maxPostponedStateSizeBytes if configured), throwing an ERR_BUFFER_TOO_LARGE error if breached to protect against zip-bomb payloads.
Sources: packages/next/src/server/resume-data-cache/resume-data-cache.ts:287-312
Runtime Postponed Execution
Shell Streaming and Resume Orchestration
Sources: packages/next/src/build/templates/app-page-runtime.ts:425-441, packages/next/src/build/templates/app-page-runtime.ts:481-494, packages/next/src/build/templates/app-page-runtime.ts:561-570
When an application page handles a request, the runtime inspects the incoming body for serialized postponed state when running in minimal mode or resuming execution. The maxPostponedStateSize configuration governs how large the incoming suspended state payload can grow before the server rejects it. If the buffer read via readBodyWithSizeLimit returns null, the request aborts immediately with a 413 status code, emitting an error message derived from getPostponedStateExceededErrorMessage.
Sources: packages/next/src/build/templates/app-page-runtime.ts:425-441
Warning
A Partially Static (PPR) shell closes its document head before any dynamic render resumes. If the user agent is identified as an HTML-limited bot via shouldServeStreamingMetadata, blocking metadata resolved during resume would be emitted after the head. In response, Next.js forces a dynamic PPR render by setting shouldForceDynamicPPRRender to true, ensuring blocking metadata remains in the initial document head.
Sources: packages/next/src/build/templates/app-page-runtime.ts:561-570
Testing APIs and Client Cache Interaction
In addition to serving standard page responses, the runtime integrates with the Segment Cache scheduler to serve prefetched static segments while dynamically evaluating missed branches on navigation.
const hasDebugStaticShellQuery =
process.env.__NEXT_EXPERIMENTAL_STATIC_SHELL_DEBUGGING === '1' &&
typeof query.__nextppronly !== 'undefined' &&
couldSupportPPR
// When enabled, this will allow the use of the `?__nextppronly` query
// to enable debugging of the fallback shell.
const hasDebugFallbackShellQuery =
hasDebugStaticShellQuery && query.__nextppronly === 'fallback'
// Whether the testing API is exposed (dev mode or explicit flag)
const exposeTestingApi =
nextConfig.cacheComponents === true &&
(routeModule.isDev === true ||
nextConfig.experimental.exposeTestingApiInProductionBuild === true)Sources: packages/next/src/build/templates/app-page-runtime.ts:445-460
The runtime uses NEXT_INSTANT_TEST_COOKIE in combination with navigation locks to evaluate whether a request represents an instant navigation test, isolating shell prefetches from runtime streaming content. On the client, spawnDynamicRequests and writeDynamicDataIntoNavigationTask parse the incoming Flight payload and update cache nodes, marking tasks as fulfilled or triggering lazy fetches when segments require additional dynamic resolution.
Sources: packages/next/src/build/templates/app-page-runtime.ts:472-478, packages/next/src/client/components/render-tree.ts:1328-1360, packages/next/src/client/components/render-tree.ts:1895-1922
Related
On This Page
OverviewDynamic Tracking MechanicsValidation and Bailout EnforcementDeclarative Opt-Outs and Abort ControllersHanging Promises ImplementationArtificial Unresolved Promises and Staged TrackingClient Hook Hanging Promises and Work Unit ValidationFallback Parameters ResolutionStaged Fallback Parameters and Segment PlaceholdersDraft Mode TrackingWork Unit Scopes and Execution ContextsMutation Tracking and Bailout HandlingVary Params AccumulationVaryParamsAccumulator MechanicsParameter Proxies and Response AccumulationPostponed State SerializationState Layout and Serialization WorkflowResume Data Cache Structures and Decompression LimitsRuntime Postponed ExecutionShell Streaming and Resume OrchestrationTesting APIs and Client Cache InteractionRelated