Skip to main content
doc0blade47/next.jsdocumentation home
Sign in

Dynamic Rendering

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

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 TypeBehavior in unstable_noStore()
prerender, prerender-client, validation-client, prerender-runtimeNo-op under Dynamic I/O
prerender-legacy, request, cache, private-cache, unstable-cache, build-time-generatorMarks 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 ValueConstant NameTarget Deopt Scope
1SessionDataShell and Prefetch
2UrlDataPrefetch only
3FallbackParamsPrefetch 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)
            )
          )
        }
        break

Sources: packages/next/src/server/app-render/dynamic-rendering-client.ts:27-46

Work Unit Store TypeuseDynamicRouteParams BehavioruseDynamicSearchParams Behavior
prerender-clientSuspends via hanging promise if fallback parameters existSuspends via hanging promise
prerender / prerender-runtimeThrows InvariantErrorThrows InvariantError
validation-clientBypasses fallback route parameter checksBypasses hanging promise for validation
cache / private-cache / unstable-cacheThrows InvariantErrorThrows InvariantError
build-time-generatorThrows InvariantError mentioning function nameThrows InvariantError mentioning function name
requestNo-opNo-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 NameInput SignaturePurpose / Behavior
createOpaqueFallbackRouteParamsfallbackRouteParams: readonly FallbackRouteParam[]Generates a randomized OpaqueFallbackRouteParams map assigning unique wrapper strings to each parameter name.
getStagedFallbackParamsroute: { fallbackRouteParams, remainingPrerenderableParams?, throwOnEmptyStaticShell? }Selects which fallback parameters are deferred for a given target shell based on build metadata and remaining prerenderable parameters.
buildDynamicSegmentPlaceholderparam: Pick<FallbackRouteParam, 'paramName' | 'paramType'>Constructs bracketed dynamic segment expressions based on parameter repeat and optional properties.
getPlaceholderFallbackRouteParamsparams: Record<string, any> | undefined, fallbackRouteParams: readonly FallbackRouteParam[]Filters fallback parameters whose active values match generated dynamic segment placeholders.
getFallbackRouteParamspage: string, routeModule: AppPageRouteModuleParses 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 TypeAction / Behavior on draftMode() Access
prerender-runtimeDelays until session data stage via stagedRendering.delayUntilStage, or retrieves cached draft mode.
requestResolves or creates cached draft mode via createOrGetCachedDraftMode.
cache, private-cache, unstable-cacheResolves provider via getDraftModeProviderForCacheScope if enclosed by a valid request store or runtime prerender.
prerender, prerender-legacyReturns an empty draft mode instance.
prerender-client, validation-clientThrows an InvariantError indicating draftMode must not be used within Client Components.
build-time-generatorThrows 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 TypeMutation Response (trackDynamicDraftMode)
cache, private-cacheThrows a mutation error created by createDraftModeMutationInUseCacheError.
unstable-cacheThrows a mutation error created by createDraftModeMutationInUnstableCacheError.
prerender, prerender-runtimeAborts and throws due to synchronous request data access without await connection().
prerender-legacySets revalidate = 0 and throws a DynamicServerError.
requestTracks dynamic data access via trackDynamicDataInDynamicRender.
build-time-generatorThrows 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 / PropertyType / SignatureBehavior
_resolve((result: IteratorResult<VaryParamId>) => void) | nullPending resolver for the async iterator.
_donebooleanTracks whether accumulation has been closed.
_bufferVaryParamId[]Stores parameter names yielded before the iterator pulls them.
_seenSet<VaryParamId>Deduplication set ensuring each parameter name is emitted at most once.
add()(id: VaryParamId) => voidRecords a parameter access, buffering or resolving immediately.
close()() => voidEnds 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

FunctionParametersReturn TypeDescription
createResponseVaryParamsAccumulatorNoneResponseVaryParamsAccumulatorCreates top-level head, rootParams, and a segments set.
createVaryParamsAccumulatorNoneVaryParamsAccumulator | nullAllocates and registers a new segment accumulator in the current work unit.
createVaryingParamsaccumulator, originalParamsObject, optionalCatchAllParamNameParamsReturns a Proxy for optional catch-alls or defineProperty getters for standard params.
createVaryingSearchParamsaccumulator, originalSearchParamsObjectSearchParamsWraps search parameters in a Proxy recording SEARCH_PARAMS_VARY_ID.
finishAccumulatingVaryParamsresponseAccumulatorvoidCloses 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

FunctionParametersReturn TypeDescription
serializePostponedStatepostponedString, resumeDataCache, isCacheComponentsEnabled, maxPostponedStateSizeBytes, disableResumeDataCacheCompressionPromise<string>Prefixes postponed payload with length, serializes and optionally compresses resume data cache via zlib.
getDynamicHTMLPostponedStatepostponed, preludeState, fallbackRouteParams, resumeDataCache, isCacheComponentsEnabled, maxPostponedStateSizeBytes, disableResumeDataCacheCompressionPromise<string>Encodes React postponed state, prelude marker state, and fallback route parameters into a string.
getDynamicDataPostponedStateresumeDataCache, isCacheComponentsEnabled, maxPostponedStateSizeBytes, disableResumeDataCacheCompression, fallbackRouteParamsPromise<string>Generates postponed state for dynamic data access without React HTML-resume requirements.
parsePostponedStatestate, maxPostponedStateSizeBytes, disableResumeDataCacheCompressionPostponedStateParses serialized postponed string back into DynamicDataPostponedState or DynamicHTMLPostponedState.
parseResumeDataCacheFromPostponedStatestate, maxPostponedStateSizeBytes, disableResumeDataCacheCompressionRenderResumeDataCacheExtracts and parses the RenderResumeDataCache portion from a postponed state string.
isEmptyHTMLPreludestatebooleanQuickly 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

Was this page helpful?