Inside next build
Follow next build through its source: route collection, Turbopack, the type check, prerendering in workers, the manifests, .next and file traces.
- 01Name the steps of next buildRead the build's output and its .next/trace file, and find each step in build/index.ts at v16.4.3.
- 02Explain the worker poolSay how many workers the build starts, how it splits the pages among them, and why a failed page stops the build.
- 03Read the .next folderFind what each manifest says and what reads it at runtime, and how .nft.json traces become the standalone folder.
One build of the tea shop, from start to finish
next build turns your project into an app that next start can serve. Earlier lessons read what it prints. Its source shows how it works: the code in Next.js that runs when you type the command, and the files it leaves behind. The source quoted is Next.js 16.4.3's, from GitHub at the tag v16.4.3.
The running example is the tea shop: a home page, a page for each tea at /teas/[id] with an Add to cart button, and a cart page that reads a cookie. Here it is, built with next build. The Terminal tab opens first. Under what the build printed, it lists what the .next folder held afterwards: each file up to two levels down, with deeper files added up into their folder.
app/teas/[id]/page.tsx1import { notFound } from "next/navigation";2import { AddToCart } from "../../add-to-cart";3import { findTea, teas } from "../../teas";4 5export function generateStaticParams() {6 return teas.map((tea) => ({ id: tea.id }));7}8 9export default async function TeaPage({ params }: PageProps<"/teas/[id]">) {10 const { id } = await params;11 const tea = findTea(id);12 if (!tea) notFound();13 return (14 <main>15 <h1>{tea.name}</h1>16 <p>${tea.price}</p>17 <AddToCart id={tea.id} />18 </main>19 );20}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (7/7) Finalizing page optimization ... Route (app)┌ ○ /├ ○ /_not-found├ ◐ /cart└ /teas/[id] ├ ◐ /teas/[id] ├ ○ /teas/green └ ○ /teas/mint ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content # What .next holds: 228 files, 29.0 MB (deeper folders added up).next/BUILD_ID 21 bytes.next/app-path-routes-manifest.json 158 bytes.next/build-manifest.json 2.3 kB.next/cache/.previewinfo 262 bytes.next/cache/.rscinfo 102 bytes.next/cache/.tsbuildinfo 121.5 kB.next/cache/turbopack/ 23.2 MB, 20 files.next/diagnostics/build-diagnostics.json 93 bytes.next/diagnostics/framework.json 37 bytes.next/diagnostics/route-bundle-stats.json 1.8 kB.next/export-marker.json 111 bytes.next/fallback-build-manifest.json 366 bytes.next/images-manifest.json 1.2 kB.next/next-minimal-server.js.nft.json 9.8 kB.next/next-server.js.nft.json 91.1 kB.next/package.json 20 bytes.next/prerender-manifest.json 9.5 kB.next/required-server-files.js 10.8 kB.next/required-server-files.json 10.8 kB.next/routes-manifest.json 2.3 kB.next/server/app-paths-manifest.json 212 bytes.next/server/app/ 229.5 kB, 85 files.next/server/chunks/ 4.7 MB, 70 files.next/server/functions-config-manifest.json 37 bytes.next/server/interception-route-rewrite-manifest.js 48 bytes.next/server/middleware-build-manifest.js 2.3 kB.next/server/middleware-manifest.json 83 bytes.next/server/next-font-manifest.js 135 bytes.next/server/next-font-manifest.json 94 bytes.next/server/pages-manifest.json 58 bytes.next/server/pages/ 17.1 kB, 2 files.next/server/prefetch-hints.json 1.1 kB.next/server/preview-props.json 250 bytes.next/server/server-reference-manifest.js 422 bytes.next/server/server-reference-manifest.json 352 bytes.next/static/Ck2PLfmuLay5_FTZYdHZb/ 417 bytes, 3 files.next/static/chunks/ 593.0 kB, 10 files.next/trace 6.6 kB.next/trace-build 1.8 kB.next/turbopack 0 bytes.next/types/cache-life.d.ts 4.6 kB.next/types/root-params.d.ts 101 bytes.next/types/routes.d.ts 1.5 kB.next/types/validator.ts 5.3 kBEach line the build printed is one step. Creating an optimized production build is the compile. Running TypeScript is the type check. Collecting page data asks each route what it is. Generating static pages (7/7) made 7 pages ahead of time. The route table comes last. The output above has no times, no worker counts and no page counter (the line a terminal draws over itself): they change from run to run, so they were removed.
The folder listing is the other half of the story. A manifest is a file that lists what the build made, so the server doesn't have to work it out again: routes-manifest.json, prerender-manifest.json and others. server/ holds the code that runs on the server, static/ the files the browser downloads, and cache/ what the next build can reuse. The steps, in order:
next build runs these steps one after another, in build/index.ts. Turbopack does step 3. Steps 5 and 6 send work to a pool of worker processes; the main process does the rest.A bakery prepares at night so the morning shift can sell. Someone reads the order book, the dough is mixed, the recipes are checked, and several bakers bake trays at the same time. Last, someone writes a sheet that says what is on which shelf. In the morning, the shop staff don't bake or read the order book again: they read the sheet and hand out what's ready.
- In real life: Reading tomorrow's order book
- In Next.js: Loading
.env,next.config.tsand the routes inapp - In real life: Mixing every dough
- In Next.js: Turbopack compiling every route
- In real life: Checking each recipe against the rules
- In Next.js: TypeScript checking the types
- In real life: Bakers baking trays side by side
- In Next.js: Workers prerendering pages
- In real life: The sheet that says what's on which shelf
- In Next.js: The manifests in
.next
next build collects your routes, compiles them with Turbopack, checks the types, prerenders what it can in a pool of worker processes, and writes manifests that describe the result. next start builds nothing: it reads those manifests and serves the files beside them. The file names and shapes in .next are this version's internals, not an API your code should use.
The steps of next build, in build/index.ts
next build starts in the CLI, which calls one function: build, at line 1116 of build/index.ts. The file is almost 5,000 lines long, but the function reads from top to bottom, one step after another. Each step is wrapped in a span: a named piece of work with a start time and a length, made by nextBuildSpan.traceChild("…"). Next.js writes the spans into a file you can open yourself: .next/trace. These are the tea shop's recorded top-level spans, the ones directly under next-build, in the order they started:
generate-buildid
load-custom-routes
create-dist-dir
clean
discover-routes
create-root-mapping
generate-route-types
public-dir-conflict-check
generate-routes-manifest
run-turbopack
turbopack-write-entrypoints
turbopack-module-graph
turbopack-emit
turbopack-persistence
run-typescript
generate-required-server-files
static-check
write-routes-manifest
static-generation
next-export
print-tree-view
write-route-bundle-stats
telemetry-flushEach name is a traceChild in build/index.ts, so you can search the file for it. Loading the settings is an earlier phase that the file doesn't show: its spans, load-dotenv and load-next-config, end before Next.js knows where .next is, and the trace writer drops a span when it doesn't know that yet (to-json.ts, lines 186 to 190), and build only tells it after the config is loaded, at lines 1245 and 1246. The settings phase first, then the spans in order, give the steps of the picture above:
- Settings, before the trace starts.
load-dotenvreads the.envfiles, thenloadConfigreadsnext.config.ts(lines 1170 to 1195). Then, in the trace,generate-buildidmakes up this build's id, andload-custom-routesreads the redirects, rewrites and headers. - A clean folder.
cleandeletes.next, but keepscache,dev,diagnostics,lockandtrace(lines 1379 to 1388). - Routes.
discover-routeswalks theappfolder forpageandroutefiles.generate-route-typeswrites the types behindPageProps<"/teas/[id]">into.next/types, andgenerate-routes-manifestbuilds the routes manifest in memory, before anything is compiled (lines 1784 to 1800). - Compile.
run-turbopackand the fourturbopack-spans under it: Turbopack, Next.js's bundler, compiles every route. - Types.
run-typescript. - Page data.
generate-required-server-fileswrites the server's settings, thenstatic-checkasks the worker pool about each route. - Prerender.
static-generation, andnext-exportinside it. - Manifests and the table.
write-routes-manifestappears twice, because the build writes that file, then updates it after prerendering.print-tree-viewprints the route table.
The compile is one call. This is how build/index.ts makes it, shortened:
Log.info('Creating an optimized production build ...')
…
if (!isGenerateMode) {
if (bundler === Bundler.Turbopack) {
const {
duration: compilerDuration,
shutdownPromise: p,
warnings,
...rest
} = await turbopackBuild(telemetry)
…
const durationString = durationToString(compilerDuration)
Log.event(`Compiled successfully in ${durationString}`)Line 1 prints Creating an optimized production build .... Line 4 checks the bundler: Turbopack, unless you ran next build --webpack. Line 10 hands the whole compile to turbopackBuild, in turbopack-build/impl.ts, which starts Turbopack's Rust code and asks it to write every route to disk: the server's code, the browser's chunks, and a small manifest for each route. Line 13 prints ✓ Compiled successfully, with the time the recording leaves out. What Turbopack does inside that call is in SWC and Turbopack.
And this is the type check, right after it:
// For app directory, we run type checking after build.
if (appDir && !isCompileMode && !isGenerateMode) {
await updateBuildDiagnostics({
buildStage: 'type-checking',
})
await startTypeChecking(typeCheckingOptions)
traceMemoryUsage('Finished type checking', nextBuildSpan)
}Line 1 says it plainly: with an app folder, types are checked after the compile. Line 6 runs TypeScript. Turbopack only removes types from your code; it never checks them. So a type error doesn't stop the compile, and the build stops one step later. Each row of this table is one step, what it does, and what it leaves in .next:
| Step | What it does | What it leaves in .next |
|---|---|---|
| Load the settings | Reads the .env files and next.config.ts, then the redirects, rewrites and headers they define. | Nothing yet: the settings say where .next is. |
| Collect the routes | Walks app and lists every page.tsx and route.ts, then writes the route types and builds the routes manifest in memory. | types/routes.d.ts and types/validator.ts. |
| Compile | Turbopack compiles every route for the server and the browser and writes the code and the per-route manifests. | server/app/…/page.js, server/chunks/, static/chunks/, the per-route manifests. |
| Check the types | Runs TypeScript over the project, after compiling, for an app with an app folder. | cache/.tsbuildinfo, which the next check reuses. |
| Collect page data | Starts the worker pool and asks it about each route: is it static, and which paths does generateStaticParams list. | required-server-files.json, written right before it. |
| Prerender | The workers render each path they can make ahead of time. | The .html, .rsc, .meta and segment files in server/app. |
| Write the manifests | Writes what the server needs to answer requests: the routes, the prerendered pages, the build's id. | routes-manifest.json, prerender-manifest.json, BUILD_ID, and more. |
| Print the route table | Prints each route with its symbol, and the legend. | diagnostics/route-bundle-stats.json. |
The order shows best when a build fails. Each version below breaks one thing in the tea shop, and each was built with real Next.js. Before you click, guess which line each build stops at.
app/teas/[id]/page.tsx1import { notFound } from "next/navigation";2import { AddToCart } from "../../add-to-cart";3import { findTea, teas } from "../../teas";4 5export function generateStaticParams() {6 return teas.map((tea) => ({ id: tea.id }));7}8 9export default async function TeaPage({ params }: PageProps<"/teas/[id]">) {10 const { id } = await params;11 const tea = findTea(id);12 if (!tea) notFound();13 return (14 <main>15 <h1>{tea.name}</h1>16 <p>${tea.price}</p>17 <AddToCart id={tea.id} />18 </main>19 );20}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (7/7) Finalizing page optimization ... Route (app)┌ ○ /├ ○ /_not-found├ ◐ /cart└ /teas/[id] ├ ◐ /teas/[id] ├ ○ /teas/green └ ○ /teas/mint ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content # What .next holds: 228 files, 29.0 MB (deeper folders added up).next/BUILD_ID 21 bytes.next/app-path-routes-manifest.json 158 bytes.next/build-manifest.json 2.3 kB.next/cache/.previewinfo 262 bytes.next/cache/.rscinfo 102 bytes.next/cache/.tsbuildinfo 121.5 kB.next/cache/turbopack/ 23.2 MB, 20 files.next/diagnostics/build-diagnostics.json 93 bytes.next/diagnostics/framework.json 37 bytes.next/diagnostics/route-bundle-stats.json 1.8 kB.next/export-marker.json 111 bytes.next/fallback-build-manifest.json 366 bytes.next/images-manifest.json 1.2 kB.next/next-minimal-server.js.nft.json 9.8 kB.next/next-server.js.nft.json 91.1 kB.next/package.json 20 bytes.next/prerender-manifest.json 9.5 kB.next/required-server-files.js 10.8 kB.next/required-server-files.json 10.8 kB.next/routes-manifest.json 2.3 kB.next/server/app-paths-manifest.json 212 bytes.next/server/app/ 229.5 kB, 85 files.next/server/chunks/ 4.7 MB, 70 files.next/server/functions-config-manifest.json 37 bytes.next/server/interception-route-rewrite-manifest.js 48 bytes.next/server/middleware-build-manifest.js 2.3 kB.next/server/middleware-manifest.json 83 bytes.next/server/next-font-manifest.js 135 bytes.next/server/next-font-manifest.json 94 bytes.next/server/pages-manifest.json 58 bytes.next/server/pages/ 17.1 kB, 2 files.next/server/prefetch-hints.json 1.1 kB.next/server/preview-props.json 250 bytes.next/server/server-reference-manifest.js 422 bytes.next/server/server-reference-manifest.json 352 bytes.next/static/Ck2PLfmuLay5_FTZYdHZb/ 417 bytes, 3 files.next/static/chunks/ 593.0 kB, 10 files.next/trace 6.6 kB.next/trace-build 1.8 kB.next/turbopack 0 bytes.next/types/cache-life.d.ts 4.6 kB.next/types/root-params.d.ts 101 bytes.next/types/routes.d.ts 1.5 kB.next/types/validator.ts 5.3 kBEvery step runs: Turbopack compiles, TypeScript finds no errors, the pages are collected and prerendered, and the route table comes last.
A missing file stops the compile: Turbopack can't follow the import, and nothing after it runs. The type error compiles, then stops at Running TypeScript, with (17,18): line 17, column 18. The page that throws gets through both, and fails while a worker prerenders /teas/oolong. Each failure stops at the first step that can see it.
Prerendering in a worker pool
A process is a program that's running, with its own memory. next build is one Node.js process, the main process. Rendering many pages in it, one after another, would leave the computer's other cores idle. So after the compile, the build starts more Node.js processes, called workers, and sends them pages to render. The group of workers is a pool.
createStaticWorker, at lines 1010 to 1057 of build/index.ts, makes the pool. Each worker loads build/worker.ts, which offers four functions to the main process: hasCustomGetInitialProps, isPageStatic, getDefinedNamedExports and exportPages. The first three answer Collecting page data. exportPages, from export/worker.ts, prerenders. The pool is built on jest-worker, a library that runs a file in several processes and spreads calls across them. Next.js uses child processes unless you turn on experimental.workerThreads (lib/worker.ts, lines 163 to 172).
How many workers
The build sizes the pool once, after the type check: getNumberOfWorkers(config, totalPageCount), at lines 2256 to 2261. The setting that counts is experimental.cpus, whose default is the computer's cores minus one (config-shared.ts, lines 2422 to 2426). Then the pool gets at most one worker more than there are routes: an app with 5 routes never starts more than 6 workers, however many cores the computer has.
How the pages are split
For prerendering, the paths are split into batches, and each batch goes to one worker: exportPagesInBatches, at lines 692 to 745 of export/index.ts. The number of batches is the paths divided by 25, rounded up (staticGenerationMinPagesPerWorker), capped at the number of workers (lines 706 to 709). Then the paths are cut, in order, into slices of the same size, one per batch, the last one sometimes shorter (lines 712 to 722). So 25 is the target that picks how many batches there are, not the size of a batch: 7 paths make 1 batch, 26 paths make 2 batches of 13, and 120 paths with 5 workers make 5 batches of 24. Inside a worker, exportPages renders up to 8 paths at a time (staticGenerationMaxConcurrency, export/worker.ts, lines 432 and 433). The tea shop on a computer with 8 cores:
Move the sliders. Start with the tea shop, then give it 120 paths: when does a second worker get a batch? Then give it 2 routes: what happens to the pool?
function getNumberOfWorkers(config: NextConfigComplete, maxTasks?: number) {
let workers: number
…
} else if (config.experimental.cpus) {
workers = config.experimental.cpus
}
…
// Cap workers to one more than the number of tasks.
// Tasks can be passed to avoid over-allocating workers.
if (maxTasks !== undefined && maxTasks > 0) {
workers = Math.min(workers, maxTasks + 1)
}
return Math.max(workers, 1)
}const minPageCountPerBatch =
nextConfig.experimental.staticGenerationMinPagesPerWorker ?? 25
// Calculate the number of workers needed to ensure each batch has at least
// minPageCountPerBatch pages.
const numWorkers = Math.min(
options.numWorkers,
Math.ceil(exportPaths.length / minPageCountPerBatch)
)
// Calculate the page count per batch based on the number of workers.
const pageCountPerBatch = Math.ceil(exportPaths.length / numWorkers)What the build would do
experimental.cpusby default: 8 cores minus one, at least 1: 7- The pool: the smaller of 7 and 5 routes plus one: 6 workers
- Batches: the smaller of 6 and 7 paths ÷ 25, rounded up: 1
- Paths in each batch, in equal slices: 7 (25 picks the count, not a batch's size)
- Rendered at once in one worker: up to 7
7 paths ÷ 25, rounded up, is 1, fewer than the 6 workers, so only 1 get a batch. The paths are cut into equal slices, one per batch, and each worker renders up to 8 of its paths at a time.
With 7 paths, the pool has 6 workers, but only one batch. At 26 paths, a second batch appears. With 2 routes, the pool shrinks to 3, however many cores there are. Your terminal prints the pool's size, like Generating static pages using 6 workers, even when one batch is all there is. Both limits, 25 and 8, can be changed in next.config.ts; they're experimental settings, so they can change in any release.
When a page fails
Open the playground above again and pick A page that throws. The last line of the Terminal says ⨯ Next.js build worker exited with code: 1 and signal: null. That's the pool at work. When /teas/oolong threw, exportPages printed the error and ended its process, because experimental.prerenderEarlyExit is on by default (export/worker.ts, lines 526 to 531). The main process watches each worker; when one exits with an error, it prints that line and exits with the same code (lib/worker.ts, lines 172 to 185). One failed page fails the whole build, so a broken page never reaches next start.
The manifests, and what reads them
After the build, next start knows nothing about your app folder. Everything it needs, the build wrote down. Turbopack writes a small manifest for each route while it compiles, and TurbopackManifestLoader merges them into the files at the top of .next (turbopack-build/impl.ts, lines 227 to 295). build/index.ts writes the rest: routes-manifest.json (line 3076), BUILD_ID (line 3098) and prerender-manifest.json (line 4619).
Two different pieces of code read them, at two different times:
- At startup, for route matching. The router server decides which route a request is for. When it starts, it calls
setupFsCheck(router-server.ts, line 177), which reads the routes manifest, the two app manifests, the middleware manifest and the pages manifest once (filesystem.ts, lines 348 to 396). - Per route, when its module handles a request. A route's compiled module calls
loadManifestseach time it prepares a request (route-module.ts, lines 873 to 877). That reads the routes, prerender, build and server reference manifests, the page's client reference manifest andrequired-server-files.json(lines 341 to 466). In production each file is read once and kept (shouldCache: !this.isDev).
Pick a manifest below and read the tea shop's:
{
"dynamicRoutes": [
{
"page": "/teas/[id]",
"regex": "^/teas/([^/]+?)(?:/)?$",
"routeKeys": {
"nxtPid": "nxtPid"
},
"namedRegex": "^/teas/(?<nxtPid>[^/]+?)(?:/)?$"
}
],
"staticRoutes": [
"/",
"/_global-error",
"/_not-found",
"/cart"
]
}- What reads it at runtime
- At startup, setupFsCheck reads it for route matching (filesystem.ts, lines 372 to 374). Per route, loadManifests loads it too: route-module.ts, lines 357 to 362
Every route as a pattern. /teas/[id] is dynamic, so it has a regular expression, with a named group for id; the other four are static. The whole file also has the redirects, rewrites and headers, and the names of the router's headers.
Read app-path-routes-manifest.json and app-paths-manifest.json side by side: one entry, /teas/[id]/page, leads to an address in one file and to a file of code in the other. The server reference manifest has the shop's one Server Action, addToCart, under an id the build made up. Its workers name app/teas/[id]/page, the route whose server code can run it. (Here "worker" means a route's server code, not a build worker.) How those ids are made, and the client reference manifests beside each page, are taught in 'use client' and 'use server' under the hood.
next build writes the manifests; next start only reads them. The router server reads the route lists once, when it starts, for route matching (filesystem.ts). A route's module reads what it needs to render when it handles a request, and in production keeps each file after the first read (route-module.ts).Each row is one manifest, what it says, and what reads it:
| File | What it says | What reads it at runtime |
|---|---|---|
routes-manifest.json | Every route as a pattern (a regular expression), plus the redirects, rewrites and headers. | At startup, for route matching: setupFsCheck (filesystem.ts, lines 372 to 374). Per route: loadManifests (route-module.ts, lines 357 to 362). |
app-path-routes-manifest.json | Which address each app entry serves: /teas/[id]/page serves /teas/[id]. | At startup, for route matching: setupFsCheck (filesystem.ts, lines 394 to 396). |
server/app-paths-manifest.json | Which compiled file holds each app entry's code: app/teas/[id]/page.js. | At startup, to know which app entries exist: setupFsCheck (filesystem.ts, lines 391 to 393). |
prerender-manifest.json | Every path made during the build, and for each dynamic route what to do with paths it didn't list. | Per route, to find a prerendered path: loadManifests (route-module.ts, lines 363 to 368). |
build-manifest.json | The browser's JavaScript files every page loads, and the old browsers' polyfills. | Per route, for the <script> tags: loadManifests (route-module.ts, lines 375 to 380). |
server/server-reference-manifest.json | Every Server Action's id, its file and name, and which routes can run it. | Per app route, to find an action's function: loadManifests (route-module.ts, lines 416 to 424). |
server/middleware-manifest.json | The code a proxy.ts or an edge route runs, and when. Empty here: the shop has neither. | At startup: setupFsCheck (filesystem.ts, lines 380 to 382). |
required-server-files.json | The settings the server runs with, and the files it can't run without. | Per route in production: loadManifests (route-module.ts, lines 432 to 439). At build time, the standalone copy step. |
Manifests are Next.js's notes to itself. Their names, fields and ids are this version's, and they change between releases without warning. Read them to understand a build or debug one. In your app's code, use the documented APIs: generateStaticParams, the route table, and the adapter API for platforms.
The .next folder you can read yourself
Go back to the tea shop's Terminal tab at the top and read the listing under the route table. The build's id is a random string, so static/ has a folder named after it; the paths below call it <buildId>. The sizes are this machine's, from one build. In the listing:
- The manifests at the top:
routes-manifest.json,prerender-manifest.json,build-manifest.json,app-path-routes-manifest.json,required-server-files.json, andBUILD_ID, a file holding only the id. server/: the server's manifests, thenserver/app/, with each route's compiled code (page.js) and the pages made during the build (.html,.rsc,.metaand segment files; Partial Prerendering reads them), andserver/chunks/, the code the routes share.static/: the browser's files./_next/static/…in the HTML points here.static/chunks/holds the JavaScript (Shrinking the JavaScript bundle measures it), andstatic/<buildId>/three small files for the browser's router.cache/: what the next build may reuse.cache/turbopack/is Turbopack's filesystem cache, andcache/.tsbuildinfois TypeScript's notes from the type check. The clean step keeps this folder.types/: the route types fromgenerate-route-types.traceandtrace-build: the spans you read above.trace-buildkeeps only the main ones, from a fixed list (to-json-build.ts).turbopack, an empty file (0 bytes): a sign that Turbopack made this build (impl.ts, lines 172 and 173).package.json, 20 bytes:{"type": "commonjs"}, so Node.js loads the server's.jsfiles the same way even when your ownpackage.jsonsays"type": "module"(impl.ts, lines 179 to 182).next-server.js.nft.jsonandnext-minimal-server.js.nft.json: traces of the server's own files.output: "standalone"uses them.
None of these files needs Next.js to read. They're JSON or text, and your editor opens them. When a build surprises you, the answer is often written in .next.
Output file tracing and standalone
Your project's node_modules holds hundreds of packages: the build tools, TypeScript, the types. The server needs only some of them. Output file tracing finds which: it reads a route's compiled code without running it, follows every import, require and file read it can identify that way, and writes the files that may be needed into a .nft.json file beside it (NFT stands for Node File Trace). In a Turbopack build, Turbopack writes these files itself while it writes the routes (crates/next-api/src/nft_json.rs); build/index.ts runs its older tracer, collect-build-traces, only for webpack builds (lines 3017 to 3021). This is the tea page's trace, from the build below:
../../../../../node_modules/@swc/helpers/cjs/_interop_require_default.cjs
../../../../../node_modules/@swc/helpers/esm/_interop_require_default.js
../../../../../node_modules/@swc/helpers/package.json
../../../../../node_modules/next/dist/build/adapter/setup-node-env.external.js
… 117 more …
../../../chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_0q-w892nagajq.js
../../../chunks/ssr/node_modules_next_dist_client_components_builtin_unauthorized_0l_sp0xky89qu.js
../../../chunks/ssr/node_modules_next_dist_compiled_@edge-runtime_cookies_index_1kiz_an963mbg.js
../../../chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1_fz5f9kvpn5j.js
page/react-loadable-manifest.json
page_client-reference-manifest.jsIt lists 127 files, each relative to the trace file itself. The first lines are in node_modules, and the packages they name are only @swc/helpers and next: React comes inside next, in the compiled files Next.js ships. The last lines are the build's own files: the route's chunks in server/chunks/ssr/ and the manifests it loads.
With output: "standalone" in next.config.ts, the build uses the traces. writeStandaloneDirectory (build/index.ts, lines 852 to 905) calls copyTracedFiles (build/utils.ts), which copies every file each route's trace lists (lines 1566 to 1578), then every file next-server.js.nft.json lists (line 1585), into .next/standalone, and writes a small server.js there. Then it copies the files required-server-files.json lists, and your .env and .env.production. Here is the tea shop built that way; the Terminal lists the folder:
next.config.ts1import type { NextConfig } from "next";2 3const nextConfig: NextConfig = {4 /* config options here */5 cacheComponents: true,6 partialPrefetching: true,7 output: "standalone",8};9 10export default nextConfig;$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (7/7) Finalizing page optimization ... Route (app)┌ ○ /├ ○ /_not-found├ ◐ /cart└ /teas/[id] ├ ◐ /teas/[id] ├ ○ /teas/green └ ○ /teas/mint ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content $ cp -r public .next/standalone/ && cp -r .next/static .next/standalone/.next/ # What .next/standalone holds: 1284 files, 46.2 MB (deeper folders added up).next/standalone/.next/BUILD_ID 21 bytes.next/standalone/.next/app-path-routes-manifest.json 158 bytes.next/standalone/.next/build-manifest.json 2.3 kB.next/standalone/.next/package.json 20 bytes.next/standalone/.next/prerender-manifest.json 9.5 kB.next/standalone/.next/required-server-files.json 10.8 kB.next/standalone/.next/routes-manifest.json 2.3 kB.next/standalone/.next/server/ 831.7 kB, 133 files.next/standalone/.next/static/ 593.4 kB, 13 files.next/standalone/node_modules/@emnapi/runtime/ 66.5 kB, 4 files.next/standalone/node_modules/@img/colour/ 46.7 kB, 3 files.next/standalone/node_modules/@img/sharp-darwin-arm64/ 281.3 kB, 3 files.next/standalone/node_modules/@img/sharp-libvips-darwin-arm64/ 18.2 MB, 6 files.next/standalone/node_modules/@img/sharp-wasm32/ 9.3 MB, 5 files.next/standalone/node_modules/@next/env/ 11.0 kB, 2 files.next/standalone/node_modules/@swc/helpers/ 30.7 kB, 5 files.next/standalone/node_modules/client-only/ 467 bytes, 2 files.next/standalone/node_modules/detect-libc/ 11.4 kB, 5 files.next/standalone/node_modules/next/ 14.6 MB, 993 files.next/standalone/node_modules/react-dom/ 1.4 MB, 12 files.next/standalone/node_modules/react/ 22.1 kB, 9 files.next/standalone/node_modules/semver/ 68.7 kB, 47 files.next/standalone/node_modules/sharp/ 631.5 kB, 29 files.next/standalone/node_modules/styled-jsx/ 22.3 kB, 4 files.next/standalone/package.json 391 bytes.next/standalone/server.js 8.0 kBLine 7 of next.config.ts turns it on. The folder's node_modules has next and @swc/helpers, from the page's trace, and also react, react-dom, styled-jsx and sharp (the image library, with its native files for this machine). The page's trace doesn't list those, so they came from the other trace copyTracedFiles reads: next-server.js.nft.json, the server's own, which Turbopack writes too (next_server_nft.rs). The recorder served the page from this folder with node .next/standalone/server.js, after copying in public and .next/static, which the server doesn't trace. The Network tab shows it answered.
.nft.json lists the files that reading its code (without running it) shows it may load, and next-server.js.nft.json lists the server's own. With output: "standalone", the build copies the files they list, and only those, into .next/standalone, beside a small server.js. A file the code loads in a way reading can't see is missing until outputFileTracingIncludes adds it.The trace is static: it lists what reading the code shows, not what a run loads. A file your code reads with a path made at runtime, like readFile(`./data/${name}.json`), or a native module a package loads in its own way, can be missing from it, and then from .next/standalone. outputFileTracingIncludes is the fix: it adds files to a route's trace. outputFileTracingExcludes removes some, and outputFileTracingRoot says where tracing starts in a monorepo. Self-hosting Next.js runs the standalone folder in Docker.
Answering questions from the .next folder
When a build does something you didn't expect, the files in .next often say why. Some questions the tea shop's team asks, and where each answer is:
- Was this page made during the build? The route table says
○or◐, andprerender-manifest.jsonlists every path that was, with its files inserver/app. - Which file holds this route's server code?
server/app-paths-manifest.json. - Why is this package in my container? Search the
.nft.jsonfiles for its name: the trace that lists it is the route that needs it, or the server itself. - Which step was slow?
.next/tracehas every span's length in microseconds, beside its name. - Will the next build reuse anything? If
.next/cachesurvives between builds, yes. A CI job that starts from an empty folder each time needs to keep it.
Sort these questions by the manifest that answers them:
- Which pattern does
/teas/oolongmatch? - Which redirects does the app have?
- Which file holds the code of the cart page?
- Which app entries did the compiler write?
- Was
/teas/mintmade during the build? - What happens to a tea
generateStaticParamsdidn't list? - Which function does an action's id stand for?
- Which routes' code can run
addToCart?
Sort each question by the file in the tea shop's .next folder that answers it.
Common mix-ups
- "next start builds whatever is missing." No.
next startbuilds nothing. It reads the manifests and serves the filesnext buildwrote; without a build, it stops with an error. - "Turbopack checks my types." No. Turbopack removes types and compiles. TypeScript checks them in the next step, which is why a type error comes after
✓ Compiled successfully. - "Generating static pages using 6 workers means six workers rendered pages." No. That's the pool's size. The number of batches is the paths divided by 25, rounded up, and capped at the workers, so a small app's pages make one batch, rendered by one worker, up to 8 at a time.
- "More cores always means more workers." No. The pool never gets more than one worker more than there are routes.
- "The manifests are an API I can build on." No. They're internal files whose names and shapes change between releases. Use the documented APIs.
- "The clean step deletes all of .next." No. It keeps
cache,dev,diagnostics,lockandtrace. - "Standalone copies my whole node_modules." No. It copies only the files the
.nft.jsontraces list: the routes' and the server's own.
Practice exercises
Answer each one first, then use the hints.
A build of the tea shop printed this. At which step did it stop?
Creating an optimized production build ...
✓ Compiled successfully
Running TypeScript ...
app/teas/[id]/page.tsx(17,18): error TS2322: Type 'number' is not assignable to type 'string'.
Failed to type check.The type check (Running TypeScript). The compile finished (✓ Compiled successfully), then TypeScript reported error TS2322 on line 17 of the tea page and the build stopped. It's the playground's version A number where a string goes.
This is the tea shop's .next/server/app-paths-manifest.json, recorded. Which file holds the cart page's server code?
{
"/_global-error/page": "app/_global-error/page.js",
"/_not-found/page": "app/_not-found/page.js",
"/cart/page": "app/cart/page.js",
"/page": "app/page.js",
"/teas/[id]/page": "app/teas/[id]/page.js"
}.next/server/app/cart/page.js. The entry /cart/page maps to app/cart/page.js, and the paths in this manifest are relative to .next/server.
A small app has 3 routes. It's built on a computer with 12 cores, with the default settings. How many workers does the pool get?
function getNumberOfWorkers(config: NextConfigComplete, maxTasks?: number) {
let workers: number
…
} else if (config.experimental.cpus) {
workers = config.experimental.cpus
}
…
// Cap workers to one more than the number of tasks.
// Tasks can be passed to avoid over-allocating workers.
if (maxTasks !== undefined && maxTasks > 0) {
workers = Math.min(workers, maxTasks + 1)
}
return Math.max(workers, 1)
}4. The default experimental.cpus is 12 minus 1, which is 11. The cap is 3 routes plus 1, which is 4, and Math.min(11, 4) is 4.
The shop grows to 120 tea pages over 4 routes, built on a computer with 8 cores. How many batches does exportPagesInBatches make, and how many paths are in each?
const minPageCountPerBatch =
nextConfig.experimental.staticGenerationMinPagesPerWorker ?? 25
// Calculate the number of workers needed to ensure each batch has at least
// minPageCountPerBatch pages.
const numWorkers = Math.min(
options.numWorkers,
Math.ceil(exportPaths.length / minPageCountPerBatch)
)
// Calculate the page count per batch based on the number of workers.
const pageCountPerBatch = Math.ceil(exportPaths.length / numWorkers)5 batches of 24 paths. The pool is the smaller of 7 and 5, so 5 workers. Math.ceil(120 / 25) is 5, and Math.min(5, 5) caps it at the workers, so numWorkers is 5. Then Math.ceil(120 / 5) is 24 paths in each. Each worker renders up to 8 of its 24 at a time.
Which function in build/index.ts decides how many workers the pool gets, and what does the line that caps it compute?
function getNumberOfWorkers(config: NextConfigComplete, maxTasks?: number) {
let workers: number
…
} else if (config.experimental.cpus) {
workers = config.experimental.cpus
}
…
// Cap workers to one more than the number of tasks.
// Tasks can be passed to avoid over-allocating workers.
if (maxTasks !== undefined && maxTasks > 0) {
workers = Math.min(workers, maxTasks + 1)
}
return Math.max(workers, 1)
}getNumberOfWorkers, at lines 979 to 1008 of build/index.ts. The line workers = Math.min(workers, maxTasks + 1) caps the pool at one more than maxTasks, the number of routes the build passes in.
The tea shop's .next/standalone/node_modules holds react-dom, but the tea page's trace below doesn't list it. Which file in .next listed it?
../../../../../node_modules/@swc/helpers/cjs/_interop_require_default.cjs
../../../../../node_modules/@swc/helpers/esm/_interop_require_default.js
../../../../../node_modules/@swc/helpers/package.json
../../../../../node_modules/next/dist/build/adapter/setup-node-env.external.js
… 117 more …
../../../chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_0q-w892nagajq.js
../../../chunks/ssr/node_modules_next_dist_client_components_builtin_unauthorized_0l_sp0xky89qu.js
../../../chunks/ssr/node_modules_next_dist_compiled_@edge-runtime_cookies_index_1kiz_an963mbg.js
../../../chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1_fz5f9kvpn5j.js
page/react-loadable-manifest.json
page_client-reference-manifest.js.next/next-server.js.nft.json. The page's trace names only next and @swc/helpers. copyTracedFiles also copies what the server's own trace lists (build/utils.ts, line 1585), and the server loads react-dom to render HTML.
Check your understanding
For each question, think about which step runs where, and which file it writes.
Question 1 of 8In a Next.js 16.4.3 app with an
appfolder, which comes first innext build?Choose an answer to see the explanation.
Question 2 of 8The tea shop prerenders 7 paths, and the pool has 6 workers. How many workers get pages to prerender?
Choose an answer to see the explanation.
Question 3 of 8Which file says that the code for
/teas/[id]/pageis inapp/teas/[id]/page.js?Choose an answer to see the explanation.
Question 4 of 8This build stopped. Which step was it in?
Read the code, then predictTextCreating an optimized production build ... ✓ Compiled successfully Running TypeScript ... app/teas/[id]/page.tsx(17,18): error TS2322: Type 'number' is not assignable to type 'string'. Failed to type check.Choose an answer to see the explanation.
Question 5 of 8In a Turbopack build, what writes
.next/server/app/teas/[id]/page.js.nft.json?Choose an answer to see the explanation.
Question 6 of 8
next buildcleans.nextbefore it builds again. Which folder does it keep?Choose an answer to see the explanation.
Question 7 of 8A prerender error ends with
⨯ Next.js build worker exited with code: 1 and signal: null. What exited?Choose an answer to see the explanation.
Question 8 of 8Can your app's code read
.next/prerender-manifest.jsonto find out which pages were prerendered?Choose an answer to see the explanation.
Key takeaways
buildin build/index.ts runs the steps in order: settings, routes, the Turbopack compile, the type check, page data, prerendering, the manifests, and the route table..next/tracelists them by name.- A failed build stops at the first step that can see the problem: a missing file in the compile, a type error after it, a throwing page while prerendering.
- Prerendering runs in a pool of worker processes: the cores minus one, at most one more than the routes. The number of batches is the paths divided by 25, rounded up and capped at the workers; the paths are cut into equal slices, one per batch, and each worker renders up to 8 at a time.
- The manifests describe the build for the server. The router server reads the route lists when it starts; each route's code reads the rest when its first request comes.
- Each route's
.nft.jsonlists the files its code loads.output: "standalone"copies those, and the server's own, into.next/standalone. - Everything in
.nextis readable, and all of it is this version's internals: read it, don't build on it.
Remember the one-liner.next build does the work once and writes down the result; next start only reads what it wrote.
Coming next: SWC and Turbopack. It opens step 3: the Rust compiler that transforms each file, and the bundler that builds one module graph for the server and the browser.