cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

How a route becomes a React tree

Follow a request through Next.js 16's app-render: the loader tree, the layouts and boundaries it nests, metadata, then the RSC payload and the HTML.

By the end, you can
  • 01
    Follow app-render's entrySay what renderToHTMLOrFlight receives, which of its answers a request gets, and what it returns.
  • 02
    Place every special fileRead a route's loader tree and its recorded payload, and tell where layout, template, error, loading, not-found and page end up.
  • 03
    Trace metadata and the two rendersExplain the order metadata merges in, and how one render to the RSC payload feeds both the HTML and the browser.

From /teas/mint to the HTML

A request for /teas/mint arrives. The server has matched it to a route, and now that route has to become a page: your layouts, templates, loading and error files and the page, put together in the right order, rendered by React, and sent as HTML. In Next.js 16.4.3 one folder of code does that job: packages/next/src/server/app-render, and its main file, app-render.tsx.

The app is the tea shop's tea pages. The folder app/teas has every special file a folder can have, and two pages: /teas/mint, made during the build, and /teas/today, which calls connection() and so is made for each request. This is a real Next.js 16.4.3 project, built with next build and run with next start. Click the files to read them:

The tea pages, with every special file in app/teasNext.js
app/teas/mint/today/
app/teas/layout.tsx
import type { Metadata } from "next"; export const metadata: Metadata = {  description: "Every tea we sell, with brewing notes.",}; export default function TeasLayout({ children }: LayoutProps<"/teas">) {  return (    <section>      <h2>Tea menu</h2>      {children}    </section>  );}
Recorded from Next.js 16.4.3
localhost:3000/teas/mintMint tea | Tea shop
Tea menu

Brewing tip: use water just below boiling.

Mint tea

Cool and fresh.

The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.

app/teas/layout.tsx, shown first, wraps every tea page in a <section> with a Tea menu heading (lines 9 to 12). Beside it, template.tsx adds a brewing tip, loading.tsx says Loading the tea…, error.tsx and not-found.tsx each show one line, and mint/page.tsx is the page. In next.config.ts, line 8 turns off Turbopack's minifier, so the built files can be read. Open the Network tab: the HTML of /teas/mint nests the page inside the template's <div>, inside the layout's <section>.

A few words first. A segment is one part of the address, and one folder of app: /teas/mint has the root segment, teas and mint. The RSC payload is the text React writes when it renders Server Components: what they rendered, and references to the Client Components, as a list of numbered lines called rows. Flight is React's name for that format and the code that writes and reads it. The browser's React reads the payload to hydrate the Client Components; Server Components never hydrate, because their code never reaches the browser. 'use client' and 'use server' under the hood showed how a client reference gets into it. Here is the whole journey, in one picture:

The loader tree goes into app-render. create-component-tree turns it into a React tree, and React's Flight server renders that to the RSC payload. The payload is copied: one copy goes to react-dom/server, which makes the HTML, and the other is wrapped in script tags. Both go into the response.loader treeapp-rendercreate-component-treeFlight: the RSC payloadSSRreact-dom/server: HTMLinlineself.__next_f.push(…)one response
One request for /teas/mint. The tree is rendered once, to the RSC payload. The HTML is made from that payload, and the payload itself rides along in <script> tags, for the browser's React.
Real-life analogyA flat-pack bookshelf

A furniture shop gets an order, picks the parts, and writes the assembly instructions. It builds the shelf from those instructions, so it arrives ready to use, and tapes the instructions inside the box. At home, you can see the shelf at once, and you still have the instructions, to change it later.

In real life: The order: which parts this bookshelf needs
In Next.js: The loader tree: which files this route needs
In real life: The factory putting the parts in the right order
In Next.js: create-component-tree and LayoutRouter nesting the files
In real life: The assembly instructions
In Next.js: The RSC payload
In real life: The shelf, built from the instructions and shipped ready
In Next.js: The HTML, rendered from the payload
In real life: The instructions taped inside the box
In Next.js: The payload inlined in the HTML, for the browser's React
The main idea

app-render turns a route's loader tree into one React tree, renders it once to the RSC payload, then makes the HTML from that payload and puts the payload inside the HTML. The server only places your special files, as props and providers; LayoutRouter, a Client Component, nests them when the tree renders.

app-render.tsx: what goes in and what comes out

app-render doesn't look at your app folder. During the build, the bundler writes one entry module for each page, from a template, build/templates/app-page.ts:

The page entry's template: app-page.ts, lines 13 to 29TypeScript
declare const tree: LoaderTree

declare const __next_app_require__: (id: string | number) => unknown
declare const __next_app_load_chunk__: (id: string | number) => Promise<unknown>

// INJECT:tree
// INJECT:__next_app_require__
// INJECT:__next_app_load_chunk__

const entrypoint = createAppPageEntrypoint({
  tree,
  page: 'VAR_DEFINITION_PAGE',
  pathname: 'VAR_DEFINITION_PATHNAME',
  require: __next_app_require__,
  loadChunk: __next_app_load_chunk__,
  interopDefault,
})

Line 1 says the template uses a variable called tree without defining it, and line 6 marks where the build puts it in. Lines 10 to 17 hand it, with the page's name and address, to createAppPageEntrypoint, which makes the route's route module: the object the server calls to render it. The recording kept the entry the build wrote for /teas/mint. This is its tree, the route's loader tree:

The loader tree of /teas/mint, from .next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1-ghz5dvi0j_g.jsJavaScript
const tree = [
    "",
    {
        "children": [
            "teas",
            {
                "children": [
                    "mint",
                    {
                        "children": [
                            "__PAGE__",
                            {},
                            {
                                metadata: {},
                                "page": [
                                    __TURBOPACK__page__$23$10__,
                                    "[project]/app/teas/mint/page.tsx"
                                ]
                            },
                            []
                        ]
                    },
                    {
                        metadata: {}
                    },
                    []
                ]
            },
            {
                metadata: {},
                "layout": [
                    __TURBOPACK__layout__$23$5__,
                    "[project]/app/teas/layout.tsx"
                ],
                "error": [
                    __TURBOPACK__error__$23$6__,
                    "[project]/app/teas/error.tsx"
                ],
                "loading": [
                    __TURBOPACK__loading__$23$7__,
                    "[project]/app/teas/loading.tsx"
                ],
                "template": [
                    __TURBOPACK__template__$23$8__,
                    "[project]/app/teas/template.tsx"
                ],
                "not-found": [
                    __TURBOPACK__not$2d$found__$23$9__,
                    "[project]/app/teas/not-found.tsx"
                ]
            },
            []
        ]
    },
    {
        "layout": [
            __TURBOPACK__layout__$23$0__,
            "[project]/app/layout.tsx"
        ],
        "not-found": [
            __TURBOPACK__not$2d$found__$23$1__,
            "[project]/node_modules/next/dist/client/components/builtin/not-found.js"
        ],
        "forbidden": [
            __TURBOPACK__forbidden__$23$2__,
            "[project]/node_modules/next/dist/client/components/builtin/forbidden.js"
        ],
        "unauthorized": [
            __TURBOPACK__unauthorized__$23$3__,
            "[project]/node_modules/next/dist/client/components/builtin/unauthorized.js"
        ],
        "global-error": [
            __TURBOPACK__global$2d$error__$23$4__,
            "[project]/node_modules/next/dist/client/components/builtin/global-error.js"
        ]
    },
    []
];

Each segment is a list of four things: its name, its children, its files, and, for a dynamic segment, the static folders beside it (empty here; app-dir-module.ts, lines 7 to 28).

  1. Line 2 is the root segment's name, "". Its files are on lines 55 to 76.
  2. Lines 4 and 5: its children slot holds the segment teas, whose files are on lines 29 to 51.
  3. Lines 7 and 8: inside it, mint. Its files, on lines 23 to 25, are only an empty metadata: the folder has no layout.
  4. Lines 10 and 11: the page is a segment of its own, __PAGE__, which Next.js adds in the loader tree (it isn't part of the address), with page.tsx on lines 15 to 18.
  5. Each file is a pair: a function that loads the module when it's needed, and the file's path. Lines 31 to 50 list all five of the tea folder's files.
  6. Lines 60 to 75: the root has no not-found.tsx, forbidden.tsx, unauthorized.tsx or global-error.tsx of yours, so the build put in Next.js's built-in ones.
  7. metadata: {} on lines 14, 24 and 30 is for metadata files like icon.png. The metadata object you export is read from each module later.

Not every request reaches app-render. /teas/mint was made during the build, so next start sent the saved file, mint.html, whole. When Next.js must render a page at request time, the route module's render calls app-render's entry, renderToHTMLOrFlight (route-modules/app-page/module.ts, lines 174 to 195). During next build, its prerender calls prerenderToHTMLOrFlight instead, which works the same way and can stop early; Prerendering and Partial Prerendering, later in this stage, reads that one. The entry, shortened from app-render.tsx, lines 3401 to 3434:

renderToHTMLOrFlight, shortened from app-render.tsxTypeScript
export const renderToHTMLOrFlight: AppPageRender = (
  req,
  res,
  pagePath,
  query,
  fallbackRouteParams,
  renderOpts,
  dev,
  sharedContext,
  routeMatch,
  parsedRequestHeaders
) => {
// …
  return workAsyncStorage.run(
    workStore,
    renderToHTMLOrFlightImpl,

Lines 2 to 11 are what it receives: the request and the response, the page's name (/teas/mint/page), the query, the route params a prerender had to leave unknown, renderOpts, which holds the route's compiled module with its loader tree, and the request's parsed router headers. Lines 14 to 16 run the rest inside workAsyncStorage, a store that any code in the render can read to learn which route it's rendering. The rest is renderAppPage, which decides what to answer, shortened from lines 3015 to 3193:

renderAppPage's answers, shortened from app-render.tsxTypeScript
if (isRSCRequest) {
// …
      // MARK: RSC dynamic
      const result = await generateDynamicFlightRenderResult(
        ctx,
        requestStore
      )
// …
if (isPossibleActionRequest) {
  // For action requests, we handle them differently with a special render result.
  const actionRequestResult = await handleAction({
// …
  const stream = await renderToStream(
    // NOTE: in Cache Components (dev), if the render is restarted, it will use a different requestStore
    // than the one that we're passing in here.
    requestStore,
    res,
    ctx,
    loaderTree,
// …
  const result = new RenderResult(stream, options)

Line 1: a request from the router, with rsc: 1, gets the payload alone; lines 4 to 7 are one of the functions that render it (there are others for prefetches and for next dev). Lines 9 to 11: a Server Action's request runs the action first. Everything else reaches line 13, renderToStream, with the loader tree on line 19, and line 21 wraps the stream it returns in a RenderResult. That's what renderToHTMLOrFlight returns: a stream of HTML that the server pipes into the response as it's made.

What renderAppPage does for each kind of request
The requestWhat renderAppPage doesWhat the response holds
A router request, with the rsc: 1 headerRenders the RSC payload alone.A text/x-component response: the payload's rows, no HTML.
A Server Action's POSTRuns the action with handleAction first.With JavaScript, an RSC response with the result and the new payload. A form posted without JavaScript gets HTML, or a 303 redirect.
Any other page requestRenders the payload, then HTML from it, with renderToStream.HTML, with the payload inlined in <script> tags.

create-component-tree: one segment at a time

renderToStream first builds the payload's tree with createFullComponentTree, in create-component-tree.tsx. It walks the loader tree from the root down, one segment at a time, and for each one, createComponentTreeInternal starts by reading the segment's files, from lines 193 to 226:

Reading one segment's files, shortened from create-component-tree.tsxTypeScript
const {
  layout,
  template,
  error,
  loading,
  'not-found': notFound,
  forbidden,
  unauthorized,
} = modules
// …
const [Template, templateStyles, templateScripts] = template
  ? await createComponentStylesAndScripts({
      ctx,
      filePath: template[1],
      getComponent: template[0],
      injectedCSS: injectedCSSWithCurrentLayout,
      injectedJS: injectedJSWithCurrentLayout,
    })
  : [Fragment]

Lines 1 to 9 take each kind of file from the segment's modules, the third item of its list in the loader tree. Lines 11 to 19 load the template's module with its CSS and scripts, or, when the folder has none, use React's Fragment, which renders only its children. The error, loading and not-found files are loaded the same way. Then the function calls itself for each child segment, and wraps each child's result in an element, from lines 696 to 718:

One element for each slot, shortened from create-component-tree.tsxTypeScript
return [
  parallelRouteKey,
  createElement(LayoutRouter, {
    parallelRouterKey: parallelRouteKey,
    error: ErrorComponent,
    errorStyles: wrappedErrorStyles,
    errorScripts: errorScripts,
    template:
// …
        : templateNode,
    templateStyles: templateStyles,
    templateScripts: templateScripts,
    notFound: notFoundComponent,
    forbidden: forbiddenComponent,
    unauthorized: unauthorizedComponent,

Line 3 makes a LayoutRouter element for the slot, usually children. Its props are this folder's files: error on line 5, template on line 8, notFound on line 13, and the two access errors on lines 14 and 15. LayoutRouter is a Client Component from client/components/layout-router.tsx, so the server doesn't render it: in the RSC layer it's a client reference, and the payload gets an import row for it. Then the folder's own layout gets the slots as props, from lines 1068 to 1077:

The layout, with its slots: create-component-tree.tsx, lines 1068 to 1077TypeScript
serverSegment = createElement(
  SegmentComponent,
  {
    ...parallelRouteProps,
    params: params,
  },
  // Force static children here so that they're validated.
  // See https://github.com/facebook/react/pull/34846
  parallelRouteProps.children
)

Line 2 is your layout component. Line 4 passes each slot as a prop, and line 9 passes children again as the element's children. So the children your TeasLayout receives is that LayoutRouter element, not the page. Last, the folder's loading.tsx goes around the whole thing, in lines 1300 to 1310:

Where loading.tsx goes: create-component-tree.tsx, lines 1300 to 1310TypeScript
if (loading !== null) {
  // If a loading.tsx boundary is present, wrap the component data in an
  // additional context provider to pass the loading data to the next
  // set of children.
  // NOTE: The reason this is a separate wrapper from LayoutRouter is because
  // not all segments render a LayoutRouter component, e.g. the root segment.
  const LoadingBoundaryProvider = ctx.componentMod.LoadingBoundaryProvider
  rsc = createElement(LoadingBoundaryProvider, {
    loading: loading,
    children: rsc,
  })

Lines 8 to 11 wrap the folder's element in a LoadingBoundaryProvider with the loading data. The comment on lines 5 and 6 says why it's a separate component: not every segment renders a LayoutRouter.

You can see the result in the recording. The recorder asked for /teas/mint's RSC payload. An import row is one row that names a Client Component's module, its chunks and an export. The page's client reference manifest, a file the build writes that maps each Client Component's module to its file and chunks, says which file each module id is. The import rows, with those files:

The import rows of /teas/mint's payload, with each module's file and exportText
3:I[39756,["$2"],"default"]   node_modules/next/dist/esm/client/components/layout-router.js, default
4:I[37457,["$2"],"default"]   node_modules/next/dist/esm/client/components/render-from-template-context.js, default
8:I[39756,["$2"],"$7"]   node_modules/next/dist/esm/client/components/layout-router.js, LoadingBoundaryProvider
a:I[75754,["$2","$9"],"default"]   app/teas/error.tsx, default
f:I[97367,["$2"],"OutletBoundary"]   node_modules/next/dist/esm/lib/framework/boundary-components.js, OutletBoundary
14:I[97367,["$2"],"$13"]   node_modules/next/dist/esm/lib/framework/boundary-components.js, ViewportBoundary
17:I[97367,["$2"],"$16"]   node_modules/next/dist/esm/lib/framework/boundary-components.js, MetadataBoundary
1a:I[68027,["$2"],"default",1]   node_modules/next/dist/client/components/builtin/global-error.js, default

Line 1 is LayoutRouter, line 2 RenderFromTemplateContext, line 3 LoadingBoundaryProvider, and line 4 your error.tsx: a "use client" file, so it's a reference too. Lines 5 to 7 are the metadata's boundaries, and line 8 Next.js's built-in global error page. The teas segment's row holds the tree for the tea folder. Written as JSX to read, with the props that only carry CSS and scripts left out, it's this:

The teas segment of the recorded payload, written as JSXJSX
<LoadingBoundaryProvider
  loading={<p>Loading the tea…</p>}
>
  <section>
    <h2>Tea menu</h2>
    <LayoutRouter
      parallelRouterKey="children"
      error={TeaError}
      template={<div><p>Brewing tip: use water just below boiling.</p><RenderFromTemplateContext /></div>}
      notFound={<p>No such tea.</p>}
    />
  </section>
</LoadingBoundaryProvider>

Lines 1 to 3: the LoadingBoundaryProvider with your loading.tsx's element. Lines 4 and 5: your TeasLayout already ran on the server; only its <section> and heading are left. Lines 6 to 11: the LayoutRouter it got as children, with error={TeaError} as a reference, the template as your <div> already rendered around RenderFromTemplateContext, and notFound as your <p>. The mint folder has no files, so its row is a bare LayoutRouter:

The mint segment of the recorded payload, written as JSXJSX
<LayoutRouter
  parallelRouterKey="children"
  template={<RenderFromTemplateContext />}
/>

Line 3: with no template.tsx, the template is RenderFromTemplateContext alone, inside the Fragment from line 19 of the first excerpt. The page itself is in a row of its own, the __PAGE__ segment. How the payload writes all this as rows is for The RSC payload, later in this stage.

Where each special file is placed

The server placed the files; LayoutRouter nests them. It runs on the server for the first HTML and in the browser after hydration, and renders, for each slot, from layout-router.tsx, lines 666 to 728:

The nesting, shortened from layout-router.tsxJSX
let templateValue = (
  <ScrollHandler renderTree={renderTree}>
    <ErrorBoundary
      errorComponent={error}
// …
      <LoadingBoundary
// …
        loading={parentLoadingData}
      >
        <HTTPAccessFallbackBoundary
// …
        >
          <RedirectBoundary>
            <InnerLayoutRouter
// …
            />
// …
  <TemplateContext.Provider key={stateKey} value={templateValue}>
    {templateStyles}
    {templateScripts}
    {template}
  </TemplateContext.Provider>
  1. Lines 18 to 22: the outside is the template. TemplateContext holds everything else, and your template's RenderFromTemplateContext takes it from there and renders it where your {children} is.
  2. Lines 3 and 4: inside the template, ErrorBoundary with your error.tsx.
  3. Lines 6 and 8: inside that, LoadingBoundary, a Suspense whose fallback is the loading data the LoadingBoundaryProvider above passed down.
  4. Line 10: inside that, HTTPAccessFallbackBoundary, with not-found.tsx, forbidden.tsx and unauthorized.tsx.
  5. Lines 13 and 14: a boundary for redirects, then InnerLayoutRouter, which renders the next segment: here mint, then the page.

Templates and the file hierarchy showed this order from the outside. Now you can say why it is what it is: the layout is outside everything, because the LayoutRouter is its child; and error.tsx can't catch an error in its own folder's layout or template, because both are outside its ErrorBoundary.

On the left, what the server put in the payload for the teas folder: LoadingBoundaryProvider with loading.tsx, around the tea layout's section, which holds a LayoutRouter with error, template and notFound props. On the right, what LayoutRouter renders from them, from the outside in: the template, the error boundary, the loading boundary, the not-found boundary, then the folder below and the page.payloadLoadingBoundaryProviderloading: loading.tsxtea layout's sectionLayoutRoutererror: TeaErrortemplate: your divnotFound: No such tea.templateerrorloadingnot-foundmint, then page
The server only places the files: as props of LayoutRouter, and loading.tsx in a provider around the folder. LayoutRouter, a Client Component, does the nesting, in the order on the right.
Where each special file of a folder ends up
FileWhere create-component-tree puts itWho renders it
layout.tsxThe folder's own element, with each slot (like children) passed in as a prop.React on the server: it's a Server Component.
page.tsxA segment of its own, __PAGE__, below its folder.React on the server, inside the folder's last LayoutRouter.
template.tsxThe template prop of the LayoutRouter in the folder's layout, around RenderFromTemplateContext.LayoutRouter, which puts the rest inside it.
error.tsxThe error prop of that LayoutRouter, as a client reference.ErrorBoundary, inside the template.
loading.tsxA LoadingBoundaryProvider around the folder's own element.LoadingBoundary, a Suspense inside the error boundary.
not-found.tsxThe notFound prop of that LayoutRouter.HTTPAccessFallbackBoundary, inside the loading boundary.

The recorded HTML shows the nesting too. This is the body of /teas/mint, from the Network tab of the first example:

The body of /teas/mint's recorded HTML, to the end of the sectionHTML
<body>
  <div hidden="">
    <!--$-->
    <!--/$-->
  </div>
  <!--&-->
  <section>
    <h2>Tea menu</h2>
    <!--&-->
    <div>
      <p>Brewing tip: use water just below boiling.</p>
      <!--$-->
      <!--&-->
      <main>
        <h1>Mint tea</h1>
        <p>Cool and fresh.</p>
      </main>
      <!--$-->
      <!--/$-->
      <!--/&-->
      <!--/$-->
    </div>
    <!--/&-->
  </section>

Lines 7 and 8 are the layout's <section> and heading. Lines 10 and 11 are the template's <div> and tip. Line 12, <!--$-->, is how React marks the start of a Suspense boundary in HTML: the loading boundary, already resolved, because the page was made during the build. The page's <main> is inside it, on lines 14 to 17. (Lines 2 to 5 are the metadata's hidden <div>, below.) /teas/today waits for a request, so its HTML arrives with the fallback in the same place:

The section of /teas/today's recorded HTMLHTML
<section>
  <h2>Tea menu</h2>
  <!--&-->
  <div>
    <p>Brewing tip: use water just below boiling.</p>
    <!--$?-->
    <template id="B:0"></template>
    <p>Loading the tea…</p>
    <!--/$-->
  </div>
  <!--/&-->
</section>

Line 8 is loading.tsx's <p>, inside the template's <div> (line 4) and after the tip: the layout and the template are on screen while the page waits. Further down, the same response streams the page in a hidden <div>, which React moves into place:

Later in /teas/today's recorded HTMLHTML
<div hidden="" id="S:1">
  <main>
    <h1>Tea of the day: Green tea</h1>
  </main>
</div>

Streaming explains the B:0 and S:1 markers. Try taking files away:

Playground: take a special file away
The tea shop, with this version of app/teasNext.js
app/teas/mint/today/
app/teas/layout.tsx
import type { Metadata } from "next"; export const metadata: Metadata = {  description: "Every tea we sell, with brewing notes.",}; export default function TeasLayout({ children }: LayoutProps<"/teas">) {  return (    <section>      <h2>Tea menu</h2>      {children}    </section>  );}
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled- Experiments (use with caution):  ⨯ turbopackMinify   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...✓ Generating static pages (4/4)  Finalizing page optimization ... Route (app)┌ ○ /_not-found├ ○ /teas/mint└ ◐ /teas/today ○  (Static)             prerendered as static content◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content # The files next build wrote for the prerendered pages, in .next/server/app# (the server's .js code and the .json lists left out), with their sizes.next/server/app/_global-error.html  9.1 kB.next/server/app/_global-error.meta  681 bytes.next/server/app/_global-error.rsc  4.6 kB.next/server/app/_global-error.segments/__PAGE__.segment.rsc  4.3 kB.next/server/app/_global-error.segments/_full.segment.rsc  4.6 kB.next/server/app/_global-error.segments/_tree.segment.rsc  115 bytes.next/server/app/_not-found.html  7.5 kB.next/server/app/_not-found.meta  774 bytes.next/server/app/_not-found.rsc  4.5 kB.next/server/app/_not-found.segments/_full.segment.rsc  4.5 kB.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc  4.0 kB.next/server/app/_not-found.segments/_tree.segment.rsc  172 bytes.next/server/app/teas/mint.html  6.9 kB.next/server/app/teas/mint.meta  903 bytes.next/server/app/teas/mint.rsc  4.4 kB.next/server/app/teas/mint.segments/_full.segment.rsc  4.4 kB.next/server/app/teas/mint.segments/_tree.segment.rsc  215 bytes.next/server/app/teas/mint.segments/teas/mint/__PAGE__.segment.rsc  3.9 kB.next/server/app/teas/today.html  1.5 kB.next/server/app/teas/today.meta  4.4 kB.next/server/app/teas/today.segments/_full.segment.rsc  4.3 kB.next/server/app/teas/today.segments/_tree.segment.rsc  216 bytes.next/server/app/teas/today.segments/teas/today/__PAGE__.segment.rsc  3.7 kB
Try it yourself
Every special file

The build lists ○ /teas/mint and ◐ /teas/today. In the Network tab, the payload of /teas/mint gives the tea layout's LayoutRouter an error, a template and a notFound.

Every version was built with next build from Next.js 16.4.3. The Terminal shows what the build printed; for the versions that built, the Network tab has /teas/mint's HTML and its RSC payload. Reset goes back to the first version.

Without template.tsx, the tea folder's payload row has template={<RenderFromTemplateContext />}, like mint's. Without loading.tsx, there's no LoadingBoundaryProvider, so no Suspense around /teas/today, and its connection() stops the build: Loading UI is, underneath, that one Suspense.

Resolving metadata

The page's <title> and <meta> tags come from the metadata exports of every layout and page on the route (The Metadata API teaches them). Before it builds the tree, getRSCPayload calls createMetadataComponents with the loader tree. When those components render, resolve-metadata.ts collects one item per segment, from lines 227 to 252:

Collecting the items, shortened from resolve-metadata.tsTypeScript
await collectMetadata({
  tree,
  metadataItems,
// …
for (const key in parallelRoutes) {
  const childTree = parallelRoutes[key]
  await resolveMetadataItemsImpl(
    metadataItems,
    childTree,
    currentTreePrefix,
    currentParams,
    optionalCatchAllParamName,
    searchParams,
    errorConvention,
    errorMetadataItem,
    interpolatedParams
  )
}

Lines 1 to 3 collect the segment's own item: its layout's export (or its page's, for __PAGE__), and its metadata files. Lines 5 to 18 then do the same for each child. So the items are in the tree's order, root first. For /teas/mint that's four: the root layout, the tea layout, mint (no layout, so nothing), and the page. Then accumulateMetadata merges them in that order, from lines 510 to 559:

Merging the items in order, shortened from resolve-metadata.tsTypeScript
for (let i = 0; i < metadataItems.length; i++) {
  const staticFilesMetadata = metadataItems[i][1]
// …

  resolvedMetadata = await mergeMetadata(route, pathname, {
    resolvedMetadata,
// …
  // If the layout is the same layer with page, skip the leaf layout and leaf page
  // The leaf layout and page are the last two items
  if (i < metadataItems.length - 2) {
    titleTemplates = {
      title: resolvedMetadata.title?.template || null,
      openGraph: resolvedMetadata.openGraph?.title.template || null,
      twitter: resolvedMetadata.twitter?.title.template || null,
    }
  }
}

Line 1 goes through the items in order. Line 5 merges each one into resolvedMetadata: a field the item sets replaces the one before. Lines 10 to 16 keep the title template for the items after it, except for the last two, the leaf's layout and page. Next.js calls each generateMetadata before this loop starts (prerenderMetadata, lines 383 to 395), and gets the parents' resolved metadata when the loop reaches it. The recorded head of /teas/mint:

The head of /teas/mint's recorded HTML, its title and descriptionHTML
<title>Mint tea | Tea shop</title>
<meta name="description" content="Every tea we sell, with brewing notes.">

Line 1: the page's Mint tea with the root layout's %s | Tea shop. Line 2: the tea layout's description replaced the root layout's.

Four metadata items in order: the root layout with a title template and a description, the tea layout with its own description, the mint folder with nothing, and the page with the title Mint tea. They merge into the title Mint tea | Tea shop and the description Every tea we sell, with brewing notes.root layout%s | Tea shoptea layoutdescriptionmintno layoutpageMint teamerged in this orderMint tea | Tea shopEvery tea we sell, with brewing notes.
resolve-metadata collects one item per segment, from the root down, then merges them in that order: a later field replaces an earlier one, and the parents' title template is applied to the page's title.

One detail of the merge surprises people. A layout that sets a title as a plain string sets the template for the segments below it to none, because a string title has no template. Try it:

Playground: whose title template applies?
The tea shop, with this version of app/teas/layout.tsxNext.js
app/teas/mint/today/
app/teas/layout.tsx
import type { Metadata } from "next"; export const metadata: Metadata = {  description: "Every tea we sell, with brewing notes.",}; export default function TeasLayout({ children }: LayoutProps<"/teas">) {  return (    <section>      <h2>Tea menu</h2>      {children}    </section>  );}
Recorded from Next.js 16.4.3

GET /teas/mint200

The whole response is .next/server/app/teas/mint.html, a file next build wrote, byte for byte (the Terminal tab lists it).

Response headers
Vary rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
x-nextjs-cache HIT
x-nextjs-prerender 1
x-nextjs-stale-time 300
X-Powered-By Next.js
Cache-Control s-maxage=31536000
Content-Type text/html; charset=utf-8

The HTML that arrived, one element per line, with the code of its 15 scripts left out

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="preload" as="script" fetchpriority="low" href="/_next/static/chunks/1o6jvcf9smbzx.js">
    <script src="/_next/static/chunks/0v8ccsftv8ajp.js" async=""></script>
    <script src="/_next/static/chunks/0kxcdnce56bv1.js" async=""></script>
    <script src="/_next/static/chunks/turbopack-1gh6_hegcwrtt.js" async=""></script>
    <script src="/_next/static/chunks/30e5tuscv0mzh.js" async=""></script>
    <script src="/_next/static/chunks/34k7bq_tk_eoi.js" async=""></script>
    <title>Mint tea | Tea shop</title>
    <meta name="description" content="Every tea we sell, with brewing notes.">
    <script src="/_next/static/chunks/0cz1d0mv5g_q7.js" nomodule=""></script>
  </head>
  <body>
    <div hidden="">
      <!--$-->
      <!--/$-->
    </div>
    <!--&-->
    <section>
      <h2>Tea menu</h2>
      <!--&-->
      <div>
        <p>Brewing tip: use water just below boiling.</p>
        <!--$-->
        <!--&-->
        <main>
          <h1>Mint tea</h1>
          <p>Cool and fresh.</p>
        </main>
        <!--$-->
        <!--/$-->
        <!--/&-->
        <!--/$-->
      </div>
      <!--/&-->
    </section>
    <!--/&-->
    <script id="_R_">…</script>
    <script src="/_next/static/chunks/1o6jvcf9smbzx.js" async=""></script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
  </body>
</html>
Try it yourself
No title in the tea layout

The tea layout sets only a description. The page's Mint tea gets the root layout's template: Mint tea | Tea shop.

Both versions were built with next build from Next.js 16.4.3 and asked for /teas/mint. The Network tab shows the <title> in the HTML's head. Reset goes back to the first version.

With title: "Teas" in the tea layout, /teas/mint's title is Mint tea. The fix is to give that layout a template of its own, like { default: "Teas", template: "%s | Tea shop" }.

Streaming metadata in Next.js 16

Where do the tags go? The metadata renders inside a hidden <div> in the body, in a Suspense of its own, from metadata.tsx, lines 123 to 140:

MetadataWrapper: metadata.tsx, lines 123 to 140JSX
function MetadataWrapper() {
  // Keep the same component structure in streaming and blocking renders.
  // The blocker only holds the shell open when metadata must not stream.
  // React requires top-level suspenseful metadata to be nested under a host
  // element. Otherwise it becomes part of the document preamble and blocks
  // shell flushing instead of streaming. Metadata tags are hoisted out, so
  // this hidden wrapper remains empty.
  return (
    <MetadataBoundary>
      <div hidden>
        <Suspense name="Next.Metadata">
          <Metadata />
        </Suspense>
      </div>
      <MetadataBlocker />
    </MetadataBoundary>
  )
}

Lines 10 to 14: a generateMetadata that waits doesn't hold up the rest of the page; its tags stream in when they're ready. React moves <title> and <meta> tags into the <head> (the comment on lines 6 and 7), so the <div hidden> stays empty: that's lines 2 to 5 of the recorded body above. Line 15's MetadataBlocker holds the page back instead, but only when serveStreamingMetadata is false, which the server decides from the visitor's User-Agent, in streaming-metadata.ts, lines 6 to 20:

shouldServeStreamingMetadata: streaming-metadata.ts, lines 6 to 20TypeScript
export function shouldServeStreamingMetadata(
  userAgent: string,
  htmlLimitedBots: string | undefined
): boolean {
  const pattern = htmlLimitedBots || HTML_LIMITED_BOT_UA_RE_STRING
  if (cachedPattern !== pattern) {
    cachedPattern = pattern
    cachedRegex = new RegExp(pattern, 'i')
  }
  // Only block metadata for HTML-limited bots
  if (userAgent && cachedRegex!.test(userAgent)) {
    return false
  }
  return true
}

Lines 11 to 13: for a bot that reads only HTML (the htmlLimitedBots setting, or Next.js's own list on line 5), metadata blocks; for everyone else it streams. The Metadata API shows that from the outside.

The RSC payload first, then the HTML

Now the tree is ready. getRSCPayload puts it, with the metadata and a few flags, into one object, shortened from app-render.tsx, lines 2107 to 2204:

What getRSCPayload builds, shortened from app-render.tsxTypeScript
const { Viewport, Metadata, MetadataOutlet } = createMetadataComponents({
  tree,
// …
const initialTree = await createFullComponentTree({
  ctx,
  loaderTree: tree,
// …
  t: {
    t: initialTree,
    h: {
      r: initialHead,
      p: isPossiblyPartialHead,
      v: getMetadataVaryParamsAccumulator(),
    },
  },
// …
  G: [GlobalError, globalErrorStyles],

Line 1 makes the metadata components, line 4 the tree that create-component-tree builds. Line 9 puts the tree under t, line 11 the head with the metadata under h, and line 17 the global error page under G. That object is what React renders. For a page rendered whole at request time on Node.js, it goes like this, shortened from app-render.tsx, lines 4047 to 4263. Prerendering during next build, and the request-time part of a ◐ page, are read in Prerendering and Partial Prerendering, later in this stage.

A request-time render on Node.js, shortened from app-render.tsxTypeScript
  const RSCPayload: RSCPayload & RSCPayloadDevProperties =
    await workUnitAsyncStorage.run(
      requestStore,
      getRSCPayload,
      tree,
      ctx,
      { is404: res.statusCode === 404, isPrerendering: false }
    )
// …
  reactServerResult = new ReactServerResult(
    workUnitAsyncStorage.run(
      requestStore,
      renderToNodeFlightStream,
      ctx.componentMod,
      RSCPayload,
      clientModules,
// …
const appElement = (
  <App
    reactServerStream={reactServerResult.tee()}
// …
const { stream: htmlStream, allReady } = await getTracer().trace(
  AppRenderSpan.renderToNodeFizzStream,
  () =>
    workUnitAsyncStorage.run(
      requestStore,
      renderToNodeFizzStream,
      appElement,
// …
return await continueFizzStream(htmlStream, {
  inlinedDataStream: createNodeInlinedDataStream(
    reactServerResult.consume(),
    nonce,
    formState
  ),
  1. Lines 1 to 8 build the payload's object with getRSCPayload.
  2. Lines 10 to 16: React's Flight server renders it to a stream, with clientModules from the client reference manifest. Your Server Components run here, once.
  3. Lines 18 to 20: the HTML render's root is App, given one copy of that stream, tee().
  4. Lines 22 to 28: renderToNodeFizzStream renders App with react-dom/server, React's HTML renderer (called Fizz inside React).
  5. Lines 30 to 35: continueFizzStream merges the HTML with the other copy of the payload, consume(), turned into script tags by createNodeInlinedDataStream.

App doesn't render your components again. It reads the payload back, from lines 2388 to 2396:

App reads the payload: app-render.tsx, lines 2388 to 2396TypeScript
preinitScripts()
const response = ReactClient.use(
  getFlightStream<InitialRSCPayload>(
    reactServerStream,
    reactDebugStream,
    debugEndTime,
    nonce
  )
)

Line 3, getFlightStream, is React's Flight client with the manifest's ssrModuleMapping, as the references lesson showed, so Client Components render from their SSR copies. Line 2's use waits for the payload, and App renders the router with it, so the HTML is exactly what the payload describes. The other copy goes into the HTML as scripts, from stream-ops.node.ts, lines 959 to 1033:

The payload into script tags, shortened from stream-ops.node.tsTypeScript
let scriptContents = `(self.__next_f=self.__next_f||[]).push(${htmlEscapeJsonString(
  JSON.stringify([INLINE_FLIGHT_PAYLOAD_BOOTSTRAP])
)})`
// …
pt.push(Buffer.from(`${startScriptTag}${scriptContents}</script>`))
// …
        const decodedString = chunk.toString('utf-8')
        htmlInlinedData = htmlEscapeJsonString(
          JSON.stringify([INLINE_FLIGHT_PAYLOAD_DATA, decodedString])
        )
// …
      output.push(
        Buffer.from(
          `${startScriptTag}self.__next_f.push(${htmlInlinedData})</script>`
        )
      )

Lines 1 to 3 and 5 write the first script, (self.__next_f=self.__next_f||[]).push([0]): an empty list on self (the browser's window). Lines 7 to 10 and 12 to 16 wrap each piece of the payload in a script that pushes [1, piece] onto it (INLINE_FLIGHT_PAYLOAD_DATA is 1). This is the recorded HTML of /teas/mint, which the recording shows is the file the build wrote, .next/server/app/teas/mint.html, sent whole. Its payload scripts, each cut short here:

The 7 payload scripts of /teas/mint's HTML, shortenedJavaScript
(self.__next_f=self.__next_f||[]).push([0])
self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:\"/_next/static/chunks/… (2270 more characters)
self.__next_f.push([1,"0:{\"P\":null,\"c\":[\"\",\"teas\",\"mint\"],\"q\… (2409 more characters)
self.__next_f.push([1,"d:C\n1c:300\n15:[[\"$\",\"meta\",\"0\",{\"charSet… (119 more characters)
self.__next_f.push([1,"11:null\n18:[[\"$\",\"title\",\"0\",{\"children\"… (137 more characters)
self.__next_f.push([1,"1c:C\n19:C\n1b:C\n12:C\nb:C\n5:C\n"])
self.__next_f.push([1,"1e:false\n1d:null\n"])

Line 1 is the empty list. Lines 2 to 7 each push a piece of the payload: the same rows the rsc: 1 request got, like 1:"$Sreact.fragment" and the root row 0:{…}. In the browser, Next.js's client reads self.__next_f and gives React the payload, so it can hydrate the Client Components without asking the server again. How the rows are read, and how the HTML and the payload stream side by side, is for The RSC payload.

Internals, not an API

The loader tree's shape, the payload's keys (t, h, G), module ids, chunk names and the self.__next_f scripts are Next.js 16.4.3's internals, from this recorded build. They can change in any release. What's promised is the documented behavior: the special files' order, metadata merging, and streaming.

Reading a route's tree yourself

In a Next.js 16.4.3 project built with Turbopack and run on Node.js, you can find these in your own build. The paths and shapes are this version's; other versions, webpack builds and the Edge runtime differ. Read them to learn and to debug, never from your app's code:

  • The loader tree: build with experimental: { turbopackMinify: false }, then search .next/server/chunks/ssr for your page's path, like [project]/app/teas/mint/page.tsx.
  • The tree as rendered: in the browser's Network tab, a client-side navigation's request with rsc: 1 has the payload, with a row for each segment.
  • The payload inside the HTML: view the page's source, and search for self.__next_f.

Sort what each part of Next.js does: which one does it?

Who places it?
  • Reading a folder's layout, template and error from the loader tree
  • Putting error.tsx into a LayoutRouter's props
  • Wrapping the page in a Suspense with loading.tsx as the fallback
  • Catching an error thrown below a folder's template
  • Applying the root layout's %s | Tea shop to the page's title
  • Letting the tea layout's description replace the root layout's
Try it yourself
0 of 6 correct

Sort each job: create-component-tree on the server, LayoutRouter, or resolve-metadata.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "A layout's children is the page." No. It's a LayoutRouter element, with the folder's template and boundaries as props. The page is further down.
  • "error.tsx wraps the layout beside it." No. The layout renders the LayoutRouter that holds ErrorBoundary, so the layout and the template are outside it.
  • "The server renders the HTML from your components, then the payload separately." No. Server Components render once, to the payload. The HTML is rendered from the payload.
  • "loading.tsx is special to Next.js's router." No. It becomes a plain Suspense fallback, placed by LoadingBoundary.
  • "The page's metadata is merged first." No. Items merge from the root layout down, so the page's fields win because they come last.
  • "The payload scripts are for search engines." No. They're for the browser's React, which reads them to hydrate the page without another request.

Practice exercises

These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints. Some ask you to find a line in Next.js's source, and the excerpts above hold it.

Exercise 1 · Warm-upFind the page in the loader tree

This is part of the recorded loader tree of /teas/mint. What is the name of the segment that holds page.tsx?

Starter codeJavaScript
                "children": [
                    "mint",
                    {
                        "children": [
                            "__PAGE__",
                            {},
                            {
                                metadata: {},
                                "page": [
                                    __TURBOPACK__page__$23$10__,
                                    "[project]/app/teas/mint/page.tsx"
                                ]
                            },
                            []
                        ]
                    },

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upRead the recorded tree

    This is the tea folder's row of the recorded payload, as JSX. What is LayoutRouter's error prop?

    Starter codeJSX
    <LoadingBoundaryProvider
      loading={<p>Loading the tea…</p>}
    >
      <section>
        <h2>Tea menu</h2>
        <LayoutRouter
          parallelRouterKey="children"
          error={TeaError}
          template={<div><p>Brewing tip: use water just below boiling.</p><RenderFromTemplateContext /></div>}
          notFound={<p>No such tea.</p>}
        />
      </section>
    </LoadingBoundaryProvider>

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFind it in the source: the Suspense boundary

      Which component in layout-router.tsx puts the page in a Suspense with loading.tsx as its fallback?

      Starter codeJSX
      let templateValue = (
        <ScrollHandler renderTree={renderTree}>
          <ErrorBoundary
            errorComponent={error}
      // …
            <LoadingBoundary
      // …
              loading={parentLoadingData}
            >
              <HTTPAccessFallbackBoundary
      // …
              >
                <RedirectBoundary>
                  <InnerLayoutRouter
      // …
                  />
      // …
        <TemplateContext.Provider key={stateKey} value={templateValue}>
          {templateStyles}
          {templateScripts}
          {template}
        </TemplateContext.Provider>

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticePredict the title

        The tea layout now sets title: "Teas". What does /teas/mint's <title> say?

        Starter codeTSX
        import type { Metadata } from "next";
        
        export const metadata: Metadata = {
          title: "Teas",
          description: "Every tea we sell, with brewing notes.",
        };
        
        export default function TeasLayout({ children }: LayoutProps<"/teas">) {
          return (
            <section>
              <h2>Tea menu</h2>
              {children}
            </section>
          );
        }
        

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeRemove loading.tsx

          You delete app/teas/loading.tsx and build. Which route stops the build?

          Starter codeTSX
          import { connection } from "next/server";
          
          export default async function TodayPage() {
            await connection();
            return (
              <main>
                <h1>Tea of the day: Green tea</h1>
              </main>
            );
          }
          

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeFind it in the source: the script tags

            On Node.js, which function turns the payload into the self.__next_f script tags?

            Starter codeTypeScript
            return await continueFizzStream(htmlStream, {
              inlinedDataStream: createNodeInlinedDataStream(
                reactServerResult.consume(),
                nonce,
                formState
              ),

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask whether the server placed it or LayoutRouter rendered it, and in which order.

              The app-render quiz · 6 questionsScore: first tries count
              1. Question 1 of 6What does renderToHTMLOrFlight give back for an ordinary page request?

                Choose an answer to see the explanation.

              2. Question 2 of 6Where does create-component-tree put a folder's error.tsx?

                Choose an answer to see the explanation.

              3. Question 3 of 6From the outside in, in what order do one folder's special files wrap the page?

                Choose an answer to see the explanation.

              4. Question 4 of 6In what order does resolve-metadata collect the metadata for /teas/mint?

                Choose an answer to see the explanation.

              5. Question 5 of 6While it makes the HTML, where does Next.js get the React tree to render?

                Choose an answer to see the explanation.

              6. Question 6 of 6What is self.__next_f in the recorded HTML of /teas/mint?

                Choose an answer to see the explanation.

              Key takeaways

              • The build writes each page's loader tree into its entry: segments from the root down, each with its files, and the page as its own __PAGE__ segment.
              • renderToHTMLOrFlight gets the request and the route's module, and returns a RenderResult: the payload alone for rsc: 1, an action's answer, or HTML.
              • create-component-tree makes, for each folder, a LayoutRouter with its template, error and not-found files as props, and a LoadingBoundaryProvider with its loading file. The layout gets the LayoutRouter as children.
              • LayoutRouter nests them: template, error, loading, not-found, then the next segment.
              • Metadata is collected from the root down and merged in that order; a string title clears the template below it. In Next.js 16, it streams in a hidden <div> unless the visitor is an HTML-only bot.
              • The tree renders once, to the RSC payload. The HTML is rendered from the payload, and the payload goes into the HTML as self.__next_f scripts.

              Remember the one-liner.
              The server places your files in one tree, renders it once to the payload, and makes the HTML from that payload.

              Coming next: The RSC payload. It reads the Flight stream row by row: the text/x-component responses, the RSC headers, and the self.__next_f data in the HTML.

              CompleteFrontend Clear concepts. Working examples.