---
title: "Pages Rendering"
description: "Pages rendering governs the server-side execution, component tree assembly, and HTML serialization pipeline for legacy Pages Router routes. It provides request dispatching through route modules, co..."
last_updated: "2026-10-12T06:38:56.887958+00:00"
canonical_url: "https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/app-rendering/pages-rendering"
---

<details>
<summary>Relevant source files</summary>

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

- [packages/next/src/server/render.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/render.tsx)
- [packages/next/src/server/base-server.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/base-server.ts)
- [packages/next/src/pages/_document.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/pages/_document.tsx)
- [packages/next/src/client/index.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/client/index.tsx)
- [packages/next/src/server/route-modules/pages/pages-handler.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/route-modules/pages/pages-handler.ts)
- [packages/next/src/build/templates/app-page-runtime.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/app-page-runtime.ts)
- [packages/next/src/server/next-server.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/next-server.ts)
- [packages/next/src/server/load-components.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/load-components.ts)
- [packages/next/src/shared/lib/router/router.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/router/router.ts)
- [packages/next/src/build/templates/pages.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/pages.ts)
- [packages/next/src/export/worker.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/worker.ts)
- [packages/next/src/pages/_app.tsx](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/pages/_app.tsx)
- [packages/next/src/export/routes/pages.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/routes/pages.ts)
- [packages/next-codemod/transforms/cra-to-next.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next-codemod/transforms/cra-to-next.ts)
- [packages/next/src/server/route-modules/pages/module.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/route-modules/pages/module.ts)
- [packages/next/src/server/render-result.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/render-result.ts)
- [packages/next/src/server/get-page-files.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/get-page-files.ts)
- [packages/next/src/shared/lib/page-path/denormalize-page-path.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/page-path/denormalize-page-path.ts)
- [packages/next/src/shared/lib/page-path/normalize-page-path.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/page-path/normalize-page-path.ts)
- [packages/next/src/shared/lib/page-path/normalize-path-sep.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/page-path/normalize-path-sep.ts)
- [packages/next/src/shared/lib/router/utils/interception-routes.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/router/utils/interception-routes.ts)
- [packages/next/src/shared/lib/router/utils/is-dynamic.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/router/utils/is-dynamic.ts)
- [packages/next/src/shared/lib/utils.ts](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/utils.ts)
</details>

## Overview

Pages rendering governs the server-side execution, component tree assembly, and HTML serialization pipeline for legacy Pages Router routes. It provides request dispatching through route modules, coordinates asset resolution with build manifests, manages asynchronous data fetching lifecycles, and integrates static site export workers.

Sources: [packages/next/src/server/route-modules/pages/module.ts:124-155](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/route-modules/pages/module.ts#L124-L155), [packages/next/src/server/render.tsx:1271-1445](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/render.tsx#L1271-L1445), [packages/next/src/export/routes/pages.ts:30-145](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/routes/pages.ts#L30-L145)

## Pages Route Module Architecture

### Route Module Layer and Request Dispatching

The Pages Router leverages a route module architecture built upon the core `RouteModule` abstraction to handle incoming requests and render pages. The compilation output template wraps userland page modules alongside custom App and Document components into a unified `PagesRouteModule` instance. This module accepts request parameters via `PagesRouteHandlerContext`, which bundles the target page identifier, parsed URL query, shared context, and render options (`RenderOpts`), omitting internal `Document` and `App` components.

```typescript
export class PagesRouteModule extends RouteModule<
  PagesRouteDefinition,
  PagesUserlandModule
> {
  private readonly components: PagesComponents

  constructor(options: PagesRouteModuleOptions) {
    super(options)

    this.components = options.components
  }

  public render(
    req: IncomingMessage,
    res: ServerResponse,
    context: PagesRouteHandlerContext
  ): Promise<RenderResult> {
    return renderToHTMLImpl(
      req,
      res,
      context.page,
      context.query,
      context.renderOpts,
      {
        App: this.components.App,
        Document: this.components.Document,
      },
      context.sharedContext,
      context.renderContext
    )
  }
}
```
Sources: [packages/next/src/server/route-modules/pages/module.ts:124-155](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/route-modules/pages/module.ts#L124-L155)

The `PagesRouteModule` class implements the `render` method, serving as the bridge that delegates execution directly to `renderToHTMLImpl`. This implementation takes node request (`IncomingMessage`) and response (`ServerResponse`) streams, alongside the context fields, and returns a promise resolving to a `RenderResult` object.

```typescript
// Create and export the route module that will be consumed.
export const routeModule = new PagesRouteModule({
  definition: {
    kind: RouteKind.PAGES,
    page: 'VAR_DEFINITION_PAGE',
    pathname: 'VAR_DEFINITION_PATHNAME',
    // The following aren't used in production.
    bundlePath: '',
    filename: '',
  },
  distDir: process.env.__NEXT_RELATIVE_DIST_DIR || '',
  relativeProjectDir: process.env.__NEXT_RELATIVE_PROJECT_DIR || '',
  components: {
    // default export might not exist when optimized for data only
    App: app.default,
    Document: document.default,
  },
  userland,
})
```
Sources: [packages/next/src/build/templates/pages.ts:46-64](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/pages.ts#L46-L64)

### Re-exports and Lifecycle Data Methods

During the build process, the pages template module acts as a re-export aggregator for userland page modules. It uses a helper utility (`hoist`) to expose default components and data-fetching hooks such as `getStaticProps`, `getStaticPaths`, and `getServerSideProps`, along with legacy aliases like `unstable_getStaticProps` and `unstable_getServerSideProps`.

| Export Identifier | Source Userland Symbol | Purpose |
| :--- | :--- | :--- |
| `default` | `userland['default']` | The main React page component |
| `getStaticProps` | `userland['getStaticProps']` | [Static generation](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/build-system/static-generation) data fetching hook |
| `getStaticPaths` | `userland['getStaticPaths']` | Dynamic route paths enumeration for SSG |
| `getServerSideProps` | `userland['getServerSideProps']` | Server-side rendering data fetching hook |
| `config` | `userland['config']` | Page-specific configuration options |
| `reportWebVitals` | `userland['reportWebVitals']` | Performance metrics reporting hook |

```typescript
// Re-export the component (should be the default export).
export default hoist(userland, 'default')

// Re-export methods.
export const getStaticProps = hoist(userland, 'getStaticProps')
export const getStaticPaths = hoist(userland, 'getStaticPaths')
export const getServerSideProps = hoist(userland, 'getServerSideProps')
export const config = hoist(userland, 'config')
export const reportWebVitals = hoist(userland, 'reportWebVitals')

// Re-export legacy methods.
export const unstable_getStaticProps = hoist(
  userland,
  'unstable_getStaticProps'
)
export const unstable_getStaticPaths = hoist(
  userland,
  'unstable_getStaticPaths'
)
export const unstable_getStaticParams = hoist(
  userland,
  'unstable_getStaticParams'
)
export const unstable_getServerProps = hoist(
  userland,
  'unstable_getServerProps'
)
export const unstable_getServerSideProps = hoist(
  userland,
  'unstable_getServerSideProps'
)
```
Sources: [packages/next/src/build/templates/pages.ts:14-44](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/build/templates/pages.ts#L14-L44)

> [!WARNING]
> In the [Edge runtime](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/server-runtime/edge-runtime), executing `Document.getInitialProps` is unsupported. If a custom Document component defines `getInitialProps` while targeting the Edge runtime, an explicit error is thrown unless a builtin functional document fallback is available.

```tsx
const BuiltinFunctionalDocument: DocumentType | undefined = (
  Document as any
)[NEXT_BUILTIN_DOCUMENT]

if (process.env.NEXT_RUNTIME === 'edge' && Document.getInitialProps) {
  // In the Edge runtime, `Document.getInitialProps` isn't supported.
  // We throw an error here if it's customized.
  if (BuiltinFunctionalDocument) {
    Document = BuiltinFunctionalDocument
  } else {
    throw new Error(
      '`getInitialProps` in Document component is not supported with the Edge Runtime.'
    )
  }
}
```
Sources: [packages/next/src/server/render.tsx:1276-1290](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/render.tsx#L1276-L1290)

## Component Loading and Assets Resolution

### Component and Document Loading Implementation

The server initializes components and resolves required assets through `loadComponents`, which delegates implementation details to `loadComponentsImpl`. For pages path executions (`isAppPath` evaluated as `false`), the loader concurrently requires the custom document (`/_document`) and app (`/_app`) modules alongside the target page module via `requirePage`. 

```typescript
let DocumentMod = {}
let AppMod = {}
if (!isAppPath) {
  ;[DocumentMod, AppMod] = await Promise.all([
    requirePage('/_document', distDir, false),
    requirePage('/_app', distDir, false),
  ])
}
```
Sources: [packages/next/src/server/load-components.ts:203-210](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/load-components.ts#L203-L210)

When `needsManifestsForLegacyReasons` is enabled, development and static generation paths evaluate retry mechanisms (`manifestLoadAttempts` set to 3 in development mode and 1 in production) to handle concurrent file compilation races when loading manifest files from disk.

```typescript
if (needsManifestsForLegacyReasons) {
  // In dev mode we retry loading a manifest file to handle a race condition
  // that can occur while app and pages are compiling at the same time, and the
  // build-manifest is still being written to disk while an app path is
  // attempting to load.
  const manifestLoadAttempts = isDev ? 3 : 1

```
Sources: [packages/next/src/server/load-components.ts:212-218](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/load-components.ts#L212-L218)

### Manifest Types and Resolution Paths

Depending on whether Turbopack or Webpack is active, manifests are fetched from designated build directories (`distDir`) using specific path constructs. For pages routes, `reactLoadableManifestPath` resolves through `normalizePagePath` to locate the correct page segment folder.

| Manifest Constant | File Name / Suffix | Loading Function / Behavior |
| :--- | :--- | :--- |
| `BUILD_MANIFEST` | `build-manifest.json` | `loadManifestWithRetries` |
| `REACT_LOADABLE_MANIFEST` | `react-loadable-manifest.json` | `tryLoadManifestWithRetries` |
| `DYNAMIC_CSS_MANIFEST` | `dynamic-css-manifest.json` | `loadManifestWithRetries` (Pages dir + Webpack + Prod) |
| `SERVER_REFERENCE_MANIFEST` | `server-reference-manifest.json` | `loadManifestWithRetries` (App path only) |
| `SUBRESOURCE_INTEGRITY_MANIFEST` | `subresource-integrity-manifest.json` | `loadManifestWithRetries` (Conditional on `sriEnabled`) |

```typescript
if (!process.env.TURBOPACK) {
  reactLoadableManifestPath = join(
    /* turbopackIgnore: true */ distDir,
    REACT_LOADABLE_MANIFEST
  )
} else if (isAppPath) {
  reactLoadableManifestPath = join(
    /* turbopackIgnore: true */ distDir,
    'server',
    'app',
    page,
    REACT_LOADABLE_MANIFEST
  )
} else {
  reactLoadableManifestPath = join(
    /* turbopackIgnore: true */ distDir,
    'server',
    'pages',
    normalizePagePath(page),
    REACT_LOADABLE_MANIFEST
  )
}
```
Sources: [packages/next/src/server/load-components.ts:220-241](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/load-components.ts#L220-L241)

> [!NOTE]
> Static metadata routes bypass client-side manifest loading (`hasClientManifest` evaluates to `false` when `isStaticMetadataRoute(page)` is true) to optimize performance.

```typescript
// Make sure to avoid loading the manifest for static metadata routes for better performance.
const hasClientManifest = !isStaticMetadataRoute(page)

```
Sources: [packages/next/src/server/load-components.ts:243-245](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/load-components.ts#L243-L245)

### Build Manifest and Page File Normalization

The core `BuildManifest` structure defines fields for runtime assets, including `devFiles`, `polyfillFiles`, `lowPriorityFiles`, `rootMainFiles`, and a mapping of `pages` where `/_app` and page segments map to read-only string arrays of chunk files.

```typescript
export type BuildManifest = {
  devFiles: readonly string[]
  polyfillFiles: readonly string[]
  lowPriorityFiles: readonly string[]
  rootMainFiles: readonly string[]
  // this is a separate field for flying shuttle to allow
  // different root main files per entries/build (ideally temporary)
  // until we can stitch the runtime chunks together safely
  rootMainFilesTree: { [appRoute: string]: readonly string[] }
  pages: {
    '/_app': readonly string[]
    [page: string]: readonly string[]
  }
  // Per-page Turbopack chunk-group bootstrap params, stored as raw JSON.
  pagesChunkGroupBootstrapParams?: { [page: string]: object }
  // The chunk-loading global the runtime drains (default "TURBOPACK").
  chunkLoadingGlobal?: string
}
```
Sources: [packages/next/src/server/get-page-files.ts:4-21](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/get-page-files.ts#L4-L21)

To locate assets for a specific route, `getPageFile` processes the path through `normalizePagePath` and `denormalizePagePath` to resolve the exact manifest key.

```typescript
export function getPageFiles(
  buildManifest: BuildManifest,
  page: string
): readonly string[] {
  const normalizedPage = denormalizePagePath(normalizePagePath(page))
  let files = buildManifest.pages[normalizedPage]

  if (!files) {
    console.warn(
      `Could not find files for ${normalizedPage} in .next/build-manifest.json`
    )
    return []
  }

  return files
}
```
Sources: [packages/next/src/server/get-page-files.ts:49-64](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/get-page-files.ts#L49-L64)

Path transformation follows specific normalization rules depending on index positioning and dynamic parameters:

| Input Page Path | Transformed Path (`normalizePagePath`) | Denormalized Path (`denormalizePagePath`) |
| :--- | :--- | :--- |
| `/` | `/index` | `/` |
| `/index` | `/index/index` | `/` |
| `/index/foo` | `/index/index/foo` | `/foo` |
| `/foo/bar` | `/foo/bar` | `/foo/bar` |

```typescript
 *  - `/` -> `/index`
 *  - `/index/foo` -> `/index/index/foo`
 *  - `/index` -> `/index/index`
 */
export function normalizePagePath(page: string): string {
  const normalized =
    /^\/index(\/|$)/.test(page) && !isDynamicRoute(page)
      ? `/index${page}`
      : page === '/'
        ? '/index'
        : ensureLeadingSlash(page)
```
Sources: [packages/next/src/shared/lib/page-path/normalize-page-path.ts:10-20](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/page-path/normalize-page-path.ts#L10-L20), [packages/next/src/shared/lib/page-path/denormalize-page-path.ts:8-18](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/page-path/denormalize-page-path.ts#L8-L18)

> [!WARNING]
> Path normalization is not idempotent for routes starting with `/index`. Repeatedly calling `normalizePagePath` nests additional `/index` segments into the path string.

```typescript
/**
 * Takes a page and transforms it into its file counterpart ensuring that the
 * output is normalized. Note this function is not idempotent because a page
 * `/index` can be referencing `/index/index.js` and `/index/index` could be
 * referencing `/index/index/index.js`. Examples:
 *  - `/` -> `/index`
 *  - `/index/foo` -> `/index/index/foo`
 *  - `/index` -> `/index/index`
 */
```
Sources: [packages/next/src/shared/lib/page-path/normalize-page-path.ts:5-13](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/page-path/normalize-page-path.ts#L5-L13)

## Data Fetching Execution Lifecycles

### Custom App Initial Props Delegation

The initialization and data population of Pages Router pages rely on utility helpers that load data before tree rendering occurs. The core utility `loadGetInitialProps` coordinates data fetching for components such as custom App entries and individual page nodes.

```typescript
export async function loadGetInitialProps<
  C extends BaseContext,
  IP = {},
  P = {},
>(App: NextComponentType<C, IP, P>, ctx: C): Promise<IP> {
  if (process.env.NODE_ENV !== 'production') {
    if (App.prototype?.getInitialProps) {
      const message = `"${getDisplayName(
        App
      )}.getInitialProps()" is defined as an instance method - visit https://nextjs.org/docs/messages/get-initial-props-as-an-instance-method for more information.`
      throw new Error(message)
    }
  }
  // when called from _app `ctx` is nested in `ctx`
  const res = ctx.res || (ctx.ctx && ctx.ctx.res)

  if (!App.getInitialProps) {
    if (ctx.ctx && ctx.Component) {
      // @ts-ignore pageProps default
      return {
        pageProps: await loadGetInitialProps(ctx.Component, ctx.ctx),
      }
    }
    return {} as IP
  }

  const props = await App.getInitialProps(ctx)

  if (res && isResSent(res)) {
    return props
  }

  if (!props) {
    const message = `"${getDisplayName(
      App
    )}.getInitialProps()" should resolve to an object. But found "${props}" instead.`
    throw new Error(message)
  }

  if (process.env.NODE_ENV !== 'production') {
    if (Object.keys(props).length === 0 && !ctx.ctx) {
      console.warn(
        `${getDisplayName(
          App
        )} returned an empty object from \`getInitialProps\`. This de-optimizes and prevents automatic static optimization. https://nextjs.org/docs/messages/empty-object-getInitialProps`
      )
    }
  }

  return props
}
```
Sources: [packages/next/src/shared/lib/utils.ts:373-423](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/utils.ts#L373-L423)

During development, `loadGetInitialProps` checks whether `getInitialProps` is improperly defined as an instance method rather than a static method on the component prototype, throwing an error if a mismatch is detected.

> [!CAUTION]
> Defining `getInitialProps` as an instance method instead of a static method on a page component or custom App triggers an immediate development-time error.

```typescript
if (process.env.NODE_ENV !== 'production') {
  if (App.prototype?.getInitialProps) {
    const message = `"${getDisplayName(
      App
    )}.getInitialProps()" is defined as an instance method - visit https://nextjs.org/docs/messages/get-initial-props-as-an-instance-method for more information.`
    throw new Error(message)
  }
}
```
Sources: [packages/next/src/shared/lib/utils.ts:378-385](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/utils.ts#L378-L385)

The custom App component (`App`) defines its default `getInitialProps` through `appGetInitialProps`, which delegates data loading directly to the active page component by invoking `loadGetInitialProps` on the supplied `Component` and `ctx` parameters.

```tsx
async function appGetInitialProps({
  Component,
  ctx,
}: AppContext): Promise<AppInitialProps> {
  const pageProps = await loadGetInitialProps(Component, ctx)
  return { pageProps }
}
```
Sources: [packages/next/src/pages/_app.tsx:26-32](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/pages/_app.tsx#L26-L32)

If a component lacks a `getInitialProps` static method entirely, `loadGetInitialProps` falls back to inspecting nested context properties or returns an empty object.

| Condition / Check | Behavior in `loadGetInitialProps` |
| :--- | :--- |
| `App.prototype.getInitialProps` (Dev-only) | Throws an error instructing the user to remove instance-level definition. |
| Missing `App.getInitialProps` with nested `ctx.Component` | Recursively loads initial props from `ctx.Component` using `ctx.ctx`. |
| Missing `App.getInitialProps` without nested component | Returns an empty object (`{} as IP`). |
| Resolved props evaluate to `null` or `undefined` | Throws an error stating `getInitialProps()` should resolve to an object. |
| Resolved props are an empty object (`Object.keys(props).length === 0`) | Emits a development warning regarding de-optimization and prevention of automatic static optimization. |

```typescript
if (process.env.NODE_ENV !== 'production') {
  if (App.prototype?.getInitialProps) {
    const message = `"${getDisplayName(
      App
    )}.getInitialProps()" is defined as an instance method - visit https://nextjs.org/docs/messages/get-initial-props-as-an-instance-method for more information.`
    throw new Error(message)
  }
}
// when called from _app `ctx` is nested in `ctx`
const res = ctx.res || (ctx.ctx && ctx.ctx.res)

if (!App.getInitialProps) {
  if (ctx.ctx && ctx.Component) {
    // @ts-ignore pageProps default
    return {
      pageProps: await loadGetInitialProps(ctx.Component, ctx.ctx),
    }
  }
  return {} as IP
}

const props = await App.getInitialProps(ctx)

if (res && isResSent(res)) {
  return props
}

if (!props) {
  const message = `"${getDisplayName(
    App
  )}.getInitialProps()" should resolve to an object. But found "${props}" instead.`
  throw new Error(message)
}

if (process.env.NODE_ENV !== 'production') {
  if (Object.keys(props).length === 0 && !ctx.ctx) {
    console.warn(
      `${getDisplayName(
        App
      )} returned an empty object from \`getInitialProps\`. This de-optimizes and prevents automatic static optimization. https://nextjs.org/docs/messages/empty-object-getInitialProps`
    )
  }
}

```
Sources: [packages/next/src/shared/lib/utils.ts:378-421](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/shared/lib/utils.ts#L378-L421)

## Component Tree Composition Flow

### Assembling the Custom App and Page Hierarchy

The render pipeline constructs the React element tree by wrapping the active page component and its custom App wrapper inside an isomorphic container structure. Within `renderContent`, the server determines whether an error state or normal route rendering is active, forwarding either the `ErrorDebug` component or the core page element tree wrapped with `AppContainerWithIsomorphicFiberStructure` and a root `Body` element (`<div id="__next">`).

```tsx
const renderContent = (_App: AppType, _Component: NextComponentType) => {
  const EnhancedApp = _App || App
  const EnhancedComponent = _Component || Component

  return ctx.err && ErrorDebug ? (
    <Body>
      <ErrorDebug />
    </Body>
  ) : (
    <Body>
      <AppContainerWithIsomorphicFiberStructure>
        {renderPageTree(EnhancedApp, EnhancedComponent, {
          ...props,
          router,
        })}
      </AppContainerWithIsomorphicFiberStructure>
    </Body>
  )
}
```
Sources: [packages/next/src/server/render.tsx:1351-1369](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/render.tsx#L1351-L1369)

The default custom App class exported by Next.js establishes the baseline component rendering behavior by extracting `Component` and `pageProps` from its instance properties and rendering the page component directly.

```tsx
export default class App<P = any, CP = {}, S = {}> extends React.Component<
  P & AppProps<CP>,
  S
> {
  static origGetInitialProps = appGetInitialProps
  static getInitialProps = appGetInitialProps

  render() {
    const { Component, pageProps } = this.props as AppProps<CP>

    return <Component {...pageProps} />
  }
```
Sources: [packages/next/src/pages/_app.tsx:34-45](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/pages/_app.tsx#L34-L45)

> [!WARNING]
> Returning `router` or `Component` properties inside `getInitialProps` from `_app.js` throws an explicit development-time error to prevent corrupting router state and element reconciliation.

```tsx
if (
  process.env.__NEXT_DEV_SERVER &&
  (props.router || props.Component)
) {
  throw new Error(
    `'router' and 'Component' can not be returned in getInitialProps from _app.js https://nextjs.org/docs/messages/cant-override-next-props`
  )
}
```
Sources: [packages/next/src/server/render.tsx:1315-1322](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/render.tsx#L1315-L1322)

## Document Shell and Script Injection

### Document Lifecycle and Initialization

The document serialization pipeline coordinates the custom or built-in `Document` component lifecycle. When rendering pages, the server inspects whether a custom `Document` defines `getInitialProps`. If an edge runtime context is active alongside a customized `Document.getInitialProps`, Next.js throws an error because the Edge Runtime does not support custom document initial props unless a built-in functional document fallback is available.

```tsx
const BuiltinFunctionalDocument: DocumentType | undefined = (
  Document as any
)[NEXT_BUILTIN_DOCUMENT]

if (process.env.NEXT_RUNTIME === 'edge' && Document.getInitialProps) {
  // In the Edge runtime, `Document.getInitialProps` isn't supported.
  // We throw an error here if it's customized.
  if (BuiltinFunctionalDocument) {
    Document = BuiltinFunctionalDocument
  } else {
    throw new Error(
      '`getInitialProps` in Document component is not supported with the Edge Runtime.'
    )
  }
}
```
Sources: [packages/next/src/server/render.tsx:1276-1290](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/render.tsx#L1276-L1290)

```tsx
const hasDocumentGetInitialProps =
  process.env.NEXT_RUNTIME !== 'edge' && !!Document.getInitialProps

```
Sources: [packages/next/src/server/render.tsx:1383-1385](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/render.tsx#L1383-L1385)

> [!CAUTION]
> Using `Document.getInitialProps` inside the Edge runtime triggers an immediate execution error, as initial props loading for documents is prohibited in lightweight edge environments.

```tsx
if (process.env.NEXT_RUNTIME === 'edge' && Document.getInitialProps) {
  // In the Edge runtime, `Document.getInitialProps` isn't supported.
  // We throw an error here if it's customized.
  if (BuiltinFunctionalDocument) {
    Document = BuiltinFunctionalDocument
  } else {
    throw new Error(
      '`getInitialProps` in Document component is not supported with the Edge Runtime.'
    )
  }
```
Sources: [packages/next/src/server/render.tsx:1280-1289](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/render.tsx#L1280-L1289)

### HtmlContext and Document Component Elements

The document component exposes layout tags (`Html`, `Main`) that consume context via `useHtmlContext`. The `Html` component captures attributes like `lang` and `data-dpl-id` from the deployment identifier while triggering script loader registration items via `handleDocumentScriptLoaderItems`. The `Main` component evaluates to a custom element tag `<next-js-internal-body-render-target />`, marking the precise insertion boundary where the rendered React component tree gets mounted into the document shell.

```tsx
export function Html(
  props: React.DetailedHTMLProps<
    React.HtmlHTMLAttributes<HTMLHtmlElement>,
    HTMLHtmlElement
  >
) {
  const {
    docComponentsRendered,
    locale,
    scriptLoader,
    deploymentId,
    __NEXT_DATA__,
  } = useHtmlContext()

  docComponentsRendered.Html = true
  handleDocumentScriptLoaderItems(scriptLoader, __NEXT_DATA__, props)

  return (
    <html
      {...props}
      lang={props.lang || locale || undefined}
      data-dpl-id={deploymentId || undefined}
    />
  )
}

export function Main() {
  const { docComponentsRendered } = useHtmlContext()
  docComponentsRendered.Main = true
  // @ts-ignore
  return <next-js-internal-body-render-target />
}
```
Sources: [packages/next/src/pages/_document.tsx:970-1001](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/pages/_document.tsx#L970-L1001)

### Script Injection and Hydration Tags

Script injection relies on manifest data retrieved via `getDocumentFiles`, which combines shared files (`/_app`) and page-specific files. Polyfill scripts are emitted first with `noModule={true}` and deferred attributes depending on optimized loading flags. Dynamic import chunks and regular page scripts are mapped with cache-busting query strings derived from asset query parameters or immutable chunk paths.

```tsx
function getDocumentFiles(
  buildManifest: BuildManifest,
  pathname: string
): DocumentFiles {
  const sharedFiles: readonly string[] = getPageFiles(buildManifest, '/_app')
  const pageFiles: readonly string[] = getPageFiles(buildManifest, pathname)

  return {
    sharedFiles,
    pageFiles,
    allFiles: [...new Set([...sharedFiles, ...pageFiles])],
  }
}

function getPolyfillScripts(context: HtmlProps, props: OriginProps) {
  // polyfills.js has to be rendered as nomodule without async
  // It also has to be the first script to load
  const {
    assetPrefix,
    buildManifest,
    assetQueryString,
    disableOptimizedLoading,
    crossOrigin,
  } = context

  return buildManifest.polyfillFiles
    .filter(
      (polyfill) => polyfill.endsWith('.js') && !polyfill.endsWith('.module.js')
    )
    .map((polyfill) => (
      <script
        key={polyfill}
        defer={!disableOptimizedLoading}
        nonce={props.nonce}
        crossOrigin={props.crossOrigin || crossOrigin}
        noModule={true}
        src={`${assetPrefix}/_next/${encodeURIPath(
          polyfill
        )}${assetQueryString}`}
      />
    ))
}
```
Sources: [packages/next/src/pages/_document.tsx:57-98](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/pages/_document.tsx#L57-L98)

```tsx
function getScripts(
  context: HtmlProps,
  props: OriginProps,
  files: DocumentFiles
) {
  const {
    assetPrefix,
    buildManifest,
    isDevelopment,
    assetQueryString,
    mutableAssetQueryString,
    disableOptimizedLoading,
    crossOrigin,
  } = context

  const normalScripts = files.allFiles.filter((file) => file.endsWith('.js'))
  const lowPriorityScripts = buildManifest.lowPriorityFiles?.filter((file) =>
    file.endsWith('.js')
  )

  const scripts = [...normalScripts, ...lowPriorityScripts].map((file) => {
    // static/immutable/chunks/51e975e7b637a580.js should use the immutable id, while
    // static/Yj152X97rfGgF7NPcJEZs/_ssgManifest.js should use the deployment id
    const query = file.startsWith('static/immutable/chunks')
      ? assetQueryString
      : mutableAssetQueryString
    return (
      <script
        key={file}
        src={`${assetPrefix}/_next/${encodeURIPath(file)}${query}`}
        nonce={props.nonce}
        async={!isDevelopment && disableOptimizedLoading}
        defer={!disableOptimizedLoading}
        crossOrigin={props.crossOrigin || crossOrigin}
      />
    )
  })
```
Sources: [packages/next/src/pages/_document.tsx:163-199](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/pages/_document.tsx#L163-L199)

For Turbopack production builds, `getInlineBootstrapScript` seeds the global runtime queue using `getTurbopackChunkGroupBootstrap` before chunk script elements drain the queue.

```tsx
function getInlineBootstrapScript(context: HtmlProps, props: OriginProps) {
  const { buildManifest, __NEXT_DATA__, crossOrigin } = context
  // Only Turbopack production builds populate these; nothing to inline otherwise.
  if (
    !buildManifest.pagesChunkGroupBootstrapParams ||
    !buildManifest.chunkLoadingGlobal
  ) {
    return null
  }
  const bootstrap = getTurbopackChunkGroupBootstrap(
    buildManifest.pagesChunkGroupBootstrapParams,
    buildManifest.chunkLoadingGlobal,
    ['/_app', __NEXT_DATA__.page]
  )
  if (!bootstrap) return null

  const html = htmlEscapeJsonString(bootstrap)
  return (
    <script
      key="turbopack-bootstrap"
      nonce={props.nonce}
      crossOrigin={props.crossOrigin || crossOrigin}
      dangerouslySetInnerHTML={{ __html: html }}
    />
  )
}
```
Sources: [packages/next/src/pages/_document.tsx:136-161](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/pages/_document.tsx#L136-L161)

```typescript
export function getTurbopackChunkGroupBootstrap(
  paramsByPage: { [page: string]: object },
  chunkLoadingGlobal: string,
  pages: readonly string[]
): string | undefined {
  const g = JSON.stringify(chunkLoadingGlobal)
  const statements = pages
    .map((page) => paramsByPage[page])
    .filter(Boolean)
    .map(
      (params) =>
        `(globalThis[${g}] || (globalThis[${g}] = [])).push(${JSON.stringify(
          params
        )});`
    )

  return statements.length > 0 ? statements.join('\n') : undefined
}
```
Sources: [packages/next/src/server/get-page-files.ts:30-47](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/server/get-page-files.ts#L30-L47)

## Static Page Export Integration

### Pages Export Execution Pipeline

The static export route handler coordinates prerendering for individual pages within the Pages router. When `exportPagesPage` processes a page component, it verifies that the component does not define `getServerSideProps`, throwing an error if server-side props are declared during export. Non-dynamic static pages with `getStaticProps` skip export rendering if build-time exports have already prerendered them, whereas dynamic routes and auto-exported components proceed through the rendering lifecycle.

```typescript
if (components.getServerSideProps) {
  throw new Error(`Error for page ${page}: ${SERVER_PROPS_EXPORT_ERROR}`)
}

// for non-dynamic SSG pages we should have already
// prerendered the file
if (!buildExport && components.getStaticProps && !isDynamic) {
  return
}
```
Sources: [packages/next/src/export/routes/pages.ts:50-58](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/routes/pages.ts#L50-L58)

> [!WARNING]
> Defining `getServerSideProps` on a page during a static export invocation triggers an immediate error, as server-side execution requires a live Node.js origin server.

```typescript
if (components.getServerSideProps) {
  throw new Error(`Error for page ${page}: ${SERVER_PROPS_EXPORT_ERROR}`)
}
```
Sources: [packages/next/src/export/routes/pages.ts:50-52](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/routes/pages.ts#L50-L52)

Path parameters and search parameters are merged into `searchAndDynamicParams` to satisfy `useRouter().query` requirements. If `optimizeCss` is enabled within render options, `process.env.__NEXT_OPTIMIZE_CSS` is injected as a stringified boolean prior to invoking `lazyRenderPagesPage`.

```typescript
const searchAndDynamicParams = {
  ...query,
  ...params,
}
```
Sources: [packages/next/src/export/routes/pages.ts:64-67](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/routes/pages.ts#L64-L67)

```typescript
if (renderOpts.optimizeCss) {
  process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true)
}
```
Sources: [packages/next/src/export/routes/pages.ts:94-96](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/routes/pages.ts#L94-L96)

### File Writer Integration and Metadata Handling

Once the render result is obtained, output files are committed to disk via the multi-file writer. If metadata contains page data and the export did not result in a `notFound` status, a companion JSON data file corresponding to `NEXT_DATA_SUFFIX` is appended to the pages data directory.

```typescript
const ssgNotFound = renderResult?.metadata.isNotFound

const html =
  renderResult && !renderResult.isNull ? renderResult.toUnchunkedString() : ''

const metadata = renderResult?.metadata || {}
if (metadata.pageData && !ssgNotFound) {
  const dataFile = join(
    pagesDataDir,
    htmlFilename.replace(/\.html$/, NEXT_DATA_SUFFIX)
  )

  fileWriter.append(dataFile, JSON.stringify(metadata.pageData))
}
```
Sources: [packages/next/src/export/routes/pages.ts:112-125](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/routes/pages.ts#L112-L125)

| Metadata Property | Target File Extension | Description |
| :--- | :--- | :--- |
| HTML Content | `.html` | Prerendered page markup string |
| `pageData` | `.json` (`_next/data/...`) | Serialized `getStaticProps` payload |
| `routeCache` | `.meta` | Cached route metadata entries |

Sources: [packages/next/src/export/routes/pages.ts:114-135](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/routes/pages.ts#L114-L135)

> [!CAUTION]
> If `getStaticProps` returns `notFound: true`, the exporter skips writing both the HTML markup file and the page data JSON file to disk entirely.

```typescript
if (!ssgNotFound) {
  // don't attempt writing to disk if getStaticProps returned not found
  fileWriter.append(htmlFilepath, html)
  if (routeCache) {
    fileWriter.append(
      htmlFilepath.replace(/\.html$/, '.meta'),
      JSON.stringify({ routeCache })
    )
  }
}
```
Sources: [packages/next/src/export/routes/pages.ts:127-136](https://github.com/blade47/next.js/blob/d8c1c1b0002db8ccb63b58095f338e8bf423fa99/packages/next/src/export/routes/pages.ts#L127-L136)

## Related

- [App Rendering](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/app-rendering/app-rendering)
- [Client Runtime](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/technical/client-runtime/client-runtime)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/8f4009b0-65bd-4480-9b00-e201f0914bb3/llms.txt) for all pages in this wiki.
