- 发布日期
第 28 讲:.next/ 目录寻宝——build 产物全景图
.next/ 产物目录全景图:chunks / server / static 的内容与读取时机
阶段四前面 5 讲都在讲"怎么 build",本讲聚焦"build 完了到底产出了什么"。
.next/目录是 Next.js 的输出根目录,里面层级深、文件多、命名隐晦。学完本讲,你能在 60 秒内定位"为什么这个 page 在生产 404"或"哪个 manifest 写错了"。
学习目标
读完本讲,你能:
- 复述
.next/目录的 7 个一级子目录及其用途。 - 列出 12 个核心 manifest 文件、知道每个文件被谁读、内容是什么。
- 区分 standalone build 与默认 build 的产物差异。
- 解读
.nft.json文件,定位 standalone 缺文件问题。 - 知道
.next/cache/的各种 cache(webpack、fetch、images)的位置和清理方式。
一、.next/ 一级结构
完整跑 pnpm build 后:
.next/
├─ BUILD_ID
├─ static/ # 客户端静态资源(_next/static/*)
├─ server/ # 服务端 bundle(page、route handler、manifest)
├─ cache/ # build/runtime cache(webpack persistent cache、fetch cache、images cache)
├─ standalone/ # output: 'standalone' 时才有
├─ types/ # 自动生成的 .d.ts(用于 IDE)
├─ trace # OpenTelemetry trace(如果开启)
├─ <manifest files> # 顶层 manifest(routes-manifest、prerender-manifest 等)
└─ <other files> # build-id、required-server-files、...
每个 build 完都会刷新(除了 cache/)。
二、顶层文件:BUILD_ID
.next/BUILD_ID → "NyDfqf-7uVRfaSqOQK3z-"
一行字符串,本次 build 的唯一标识。前面讲过它的 3 个用途:CDN cache key、chunk URL、manifest 标识。
排障:部署后客户端报 MissingClientReferenceManifest,常见原因是 buildId 在不同实例间不一致(多容器部署没共享文件)。
三、静态产物:.next/static/
.next/static/
├─ chunks/
│ ├─ framework-<hash>.js
│ ├─ webpack-<hash>.js
│ ├─ main-app-<hash>.js
│ ├─ pages/ # Pages Router 的 page chunk
│ ├─ app/ # App Router 的 page chunk
│ │ └─ blog/[slug]/page-<hash>.js
│ ├─ <numeric-hash>.js # async chunks (dynamic import)
├─ css/
│ ├─ <hash>.css # CSS bundles
├─ media/
│ ├─ <hash>.woff2 # next/font 字体
│ ├─ <hash>.jpg # static image 的 placeholder
├─ <buildId>/
│ └─ _ssgManifest.js # 客户端读,了解 SSG page 列表
└─ chunks/pages/_app-<hash>.js
URL 形式:/_next/static/chunks/framework-abc.js。这些是 client public asset,部署到 CDN,配 Cache-Control: public, max-age=31536000, immutable。
业务陷阱:
/_next/static/*的 hash 跨 build 不稳定,部署新版本时 CDN 边缘节点要主动 purge 或等 max-age 过期。Vercel 自动处理;自托管要手动配。
四、服务端产物:.next/server/
.next/server/
├─ app/ # App Router 的 server bundle + 预渲染产物
│ ├─ page.js # 编译后的 page module
│ ├─ page.js.map # source map
│ ├─ page.html # prerender HTML(静态/PPR shell)
│ ├─ page.rsc # prerender RSC payload
│ ├─ page.meta # prerender metadata(cache control、tags)
│ ├─ page_client-reference-manifest.js # 每个 page 一个
│ ├─ page.js.nft.json # nft trace
│ └─ blog/
│ └─ [slug]/
│ ├─ page.js
│ └─ ...
├─ pages/ # Pages Router 的产物(如果有)
├─ chunks/ # server-side chunks
├─ edge/ # Edge Runtime bundle
│ └─ middleware.js
├─ functions-config-manifest.json
├─ app-paths-manifest.json
├─ pages-manifest.json
├─ middleware-manifest.json
├─ next-font-manifest.json
├─ webpack-runtime.js
├─ webpack-api-runtime.js
└─ font-manifest.json
注意:.html .rsc .meta 三件套是预渲染产物。运行时(server start 后)某个 page 第一次被请求,base-server 看到这条路径在 prerender-manifest 里、对应文件存在,直接 stream 这些预产物,不重新调 page module。
五、12 个核心 manifest
按重要性递减排列:
1. prerender-manifest.json
{
"version": 4,
"routes": {
"/about": {
"experimentalBypassFor": null,
"initialRevalidateSeconds": false,
"srcRoute": null,
"dataRoute": "/about.rsc",
"experimentalPPR": false
},
"/blog/hello": {
"initialRevalidateSeconds": 60,
"srcRoute": "/blog/[slug]",
"dataRoute": "/blog/hello.rsc"
}
},
"dynamicRoutes": {
"/blog/[slug]": {
"routeRegex": "^/blog/([^/]+?)(?:/)?$",
"dataRoute": "/blog/[slug].rsc",
"fallback": null // null = blocking, false = NOT_FOUND
}
},
"preview": { ... },
"notFoundRoutes": []
}
谁读:base-server.findPrerenderedPath() 在每次请求时读,决定是直接 stream prerender 产物还是 dynamic render。
2. routes-manifest.json
{
"version": 3,
"basePath": "",
"redirects": [...],
"rewrites": { "beforeFiles": [...], "afterFiles": [...], "fallback": [...] },
"headers": [...],
"staticRoutes": [
{ "page": "/about", "regex": "^/about(?:/)?$", "namedRegex": "^/about(?:/)?$" }
],
"dynamicRoutes": [
{ "page": "/blog/[slug]", "regex": "^/blog/([^/]+?)(?:/)?$", "namedRegex": "..." }
],
"rsc": { ... }
}
谁读:router-server 的 resolveRoutes 用它做路径匹配、应用 rewrite / redirect / header。
3. app-paths-manifest.json
{
"/page": "app/page.js",
"/blog/[slug]/page": "app/blog/[slug]/page.js",
"/api/hello/route": "app/api/hello/route.js"
}
谁读:base-server.findPageComponents(),把 normalized pathname 映射到 server bundle 文件路径。
4. middleware-manifest.json
{
"sortedMiddleware": ["/"],
"middleware": {
"/": {
"files": ["server/edge-runtime-webpack.js", "server/middleware.js"],
"matchers": [{"regexp": "^/.*$"}],
"wasm": [],
"name": "middleware",
"page": "/"
}
},
"functions": {
"/api/edge/health": {
"files": [...],
"matchers": [{"regexp": "^/api/edge/health$"}],
"page": "/api/edge/health"
}
},
"version": 3
}
谁读:router-server 决定走 NextNodeServer 还是 NextWebServer。
5. <page>_client-reference-manifest.js
每个 App Router page 一个。注入 globalThis.__RSC_MANIFEST['/path']。第 24 讲讲过结构。
谁读:RSC render 时,server 渲染 client component 边界,从这里取 chunk URL 编码到 RSC payload。
6. build-manifest.json
{
"polyfillFiles": ["static/chunks/polyfills-abc.js"],
"devFiles": [],
"ampDevFiles": [],
"lowPriorityFiles": [],
"rootMainFiles": ["static/chunks/webpack-abc.js", "static/chunks/framework-def.js", "static/chunks/main-app-ghi.js"],
"pages": {
"/_app": [...],
"/_error": [...]
},
"ampFirstPages": []
}
谁读:Pages Router 的 <Document> render 时拼接 <script src> 列表;App Router 也部分依赖。
7. app-build-manifest.json
{
"pages": {
"/page": ["static/chunks/app/page-abc.js"],
"/blog/[slug]/page": ["static/chunks/app/blog/[slug]/page-def.js"]
}
}
谁读:App Router 渲染时找该 page 需要 inline 的 client chunks。
8. pages-manifest.json
Pages Router 的路径映射(类似 app-paths-manifest)。
9. next-font-manifest.json
{
"pages": {},
"app": {
"/layout": ["static/css/font-abc.css"]
},
"appUsingSizeAdjust": true
}
谁读:SSR render 时注入 <link rel="preload" as="font"> + <link rel="stylesheet">(字体相关 CSS)。
10. images-manifest.json
next.config.js 的 images 配置的副本,被 image-optimizer 读。
11. react-loadable-manifest.json
next/dynamic 的 lazy chunk 映射,运行时知道某 dynamic import 对应哪个 chunk。
12. required-server-files.json
{
"version": 1,
"config": { ... }, // 部分 next.config.js
"appDir": "/proj/app",
"files": [
".next/server/pages/_app.js",
".next/server/middleware-manifest.json",
// ...
],
"ignore": ["node_modules/@swc/core-linux-x64-musl/..."]
}
谁读:standalone 构建器 + Vercel 部署器,知道运行时需要复制哪些文件。
六、functions-config-manifest.json
App Router 1.0 之后引入。每个 route handler 的 runtime 配置:
{
"version": 1,
"functions": {
"/api/hello": {
"runtime": "nodejs",
"regions": ["all"],
"maxDuration": 60
},
"/api/edge/health": {
"runtime": "edge"
}
}
}
Vercel / Cloudflare 部署 adapter 读这个文件,分配 region / function。
七、Cache 目录:.next/cache/
.next/cache/
├─ webpack/ # webpack 5 persistent cache(按 compiler 分目录)
│ ├─ client-production/
│ ├─ server-production/
│ └─ edge-server-production/
├─ turbopack/ # turbopack 持久化 cache
├─ fetch-cache/ # build 期间 fetch() 拉到的数据 cache
│ └─ <hash> # 每个 fetch URL 一个文件
├─ images/ # next/image 运行时缓存
│ └─ <hash>/
│ ├─ 0.<etag>.<expires>.webp
├─ swc/ # SWC 编译缓存
└─ next-server/ # 一些 runtime 中间产物
清理建议:
- 改 next.config.js 但 build 行为没变 → 删
.next/cache/webpack/ - turbopack 行为诡异 → 删
.next/cache/turbopack/ - ISR 数据顽固 → 删
.next/cache/fetch-cache/或运行时revalidateTag
cache/ 在多次 build 之间是保留的,加速增量 build。某些 CI 会把它单独 cache(如 GitHub Actions 的 cache@v3)。
八、Standalone 目录
next.config.js 设 output: 'standalone' 时:
.next/standalone/
├─ server.js # 自包含启动入口
├─ package.json # 简化版
├─ .next/
│ ├─ server/ # 复制 .next/server/ 大部分
│ ├─ standalone-manifest.json
│ └─ ...
├─ node_modules/ # 只包含 nft trace 选中的依赖
│ ├─ next/
│ ├─ react/
│ └─ ...(其它运行时依赖)
└─ public/ # 复制 ./public/
server.js 直接 node server.js 即可启动,不需要安装任何 node_modules。
部署到容器场景的核心模式:
FROM node:20-alpine AS builder
COPY . .
RUN pnpm install --frozen-lockfile
RUN pnpm build # 会输出 .next/standalone/
FROM node:20-alpine AS runner
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
COPY /app/public ./public
CMD ["node", "server.js"]
最终镜像可能只有 200-500MB(不再带 node_modules),冷启动也更快。
九、.nft.json:standalone 的关键文件
回顾第 23 讲。每个 server entry 都有:
.next/server/app/page.js.nft.json
.next/server/app/blog/[slug]/page.js.nft.json
.next/server/middleware.js.nft.json
.next/server/app/api/hello/route.js.nft.json
文件内容:
{
"version": 1,
"files": [
"../../../node_modules/next/dist/server/app-render.js",
"../../../node_modules/react/index.js",
"../../../node_modules/sharp/lib/index.js",
"../../../public/data.json"
]
}
standalone 复制时按这个 list 复制对应文件。
排障:
# 看某个 page 依赖了什么
cat .next/server/app/page.js.nft.json | jq -r '.files[]' | sort | uniq -c | sort -n
# 检查某个 module 是否被 nft 抓到
cat .next/server/app/page.js.nft.json | jq -r '.files[]' | grep my-lib
十、Types 目录:.next/types/
.next/types/
├─ app/
│ ├─ page.ts # 自动生成 PageProps / LayoutProps 类型
│ ├─ blog/[slug]/page.ts
└─ ...
App Router 用 TypeScript 时,Next.js 会生成 .next/types/app/<route>/page.ts,让你写:
import { PageProps } from "next";
export default function Page({
params,
searchParams,
}: PageProps<"/blog/[slug]">) {
// params 自动推断为 { slug: string }
}
tsconfig.json 的 plugins: [{ name: 'next' }] 会让 IDE 加载这些类型。
十一、trace 文件
如果开启 OpenTelemetry:
.next/trace
一个 NDJSON 文件,每行一个 span。可上传到 Datadog/Honeycomb:
next build --upload-trace
或本地解析:
cat .next/trace | jq -c 'select(.name | startswith("next-build"))' | head
十二、生产排障速查表
| 现象 | 检查文件 |
|---|---|
| 404 某动态路由 | prerender-manifest.json 的 dynamicRoutes 里有这条吗?fallback 是 false 吗? |
| rewrite/redirect 不生效 | routes-manifest.json 的 rewrites/redirects |
| client component 找不到 | <page>_client-reference-manifest.js 是否生成 |
| middleware 不触发 | middleware-manifest.json 的 matchers 是否覆盖 |
| edge route 跑成 nodejs | functions-config-manifest.json 的 runtime |
standalone Cannot find module | <page>.nft.json 是否缺这个文件;用 outputFileTracingIncludes 补 |
| 字体没 preload | next-font-manifest.json 是否包含本 layout |
| ISR revalidate 失效 | prerender-manifest.json 的 initialRevalidateSeconds |
| build 慢但产物没变 | .next/cache/ 持久化生效;删掉测试 cold build 时间 |
十三、配套 fixture:动手探索
fixtures/lecture-28/ 提供一个综合 demo(静态 + ISR + dynamic + edge route + middleware + standalone):
cd learning/nextjs-40-lectures/fixtures/lecture-28
pnpm install
# 1. 默认 build
pnpm build
ls .next/
ls .next/server/app/
ls .next/static/
# 2. 12 个 manifest 一览
ls .next/ | grep -i manifest
ls .next/server/ | grep -i manifest
# 3. standalone build
pnpm build:standalone
ls .next/standalone/
提供的 scripts/inspect.sh 一键 dump 关键 manifest 内容:
bash scripts/inspect.sh
十四、本讲小结
.next/7 个一级目录:static / server / cache / standalone / types / trace + 顶层 manifest。- 12 个核心 manifest 是 build 与 runtime 的契约;排障第一步是查 manifest。
.nft.json决定 standalone 复制哪些文件,缺文件主要找它。.next/cache/是 build 加速器,但偶尔需要清理。output: 'standalone'是容器部署的最佳实践——自包含、小镜像、快冷启动。
阶段四完结 → 进入阶段五
至此第 23-28 讲(阶段四:构建、打包与优化)全部完成。下一讲进入阶段五:运行时与部署。
下讲预告:第 29 讲:Node Server / Standalone / Adapter 三种部署形态。我们会拆 next-start.ts、standalone server.js、Vercel adapter 的差异,以及 process 启动流程、port binding、graceful shutdown。