发布日期

第 28 讲:.next/ 目录寻宝——build 产物全景图

.next/ 产物目录全景图:chunks / server / static 的内容与读取时机

阶段四前面 5 讲都在讲"怎么 build",本讲聚焦"build 完了到底产出了什么"。.next/ 目录是 Next.js 的输出根目录,里面层级深、文件多、命名隐晦。学完本讲,你能在 60 秒内定位"为什么这个 page 在生产 404"或"哪个 manifest 写错了"。

学习目标

读完本讲,你能:

  1. 复述 .next/ 目录的 7 个一级子目录及其用途。
  2. 列出 12 个核心 manifest 文件、知道每个文件被谁读、内容是什么。
  3. 区分 standalone build 与默认 build 的产物差异。
  4. 解读 .nft.json 文件,定位 standalone 缺文件问题。
  5. 知道 .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.jsoutput: '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 --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /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.jsonplugins: [{ 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.jsondynamicRoutes 里有这条吗?fallback 是 false 吗?
rewrite/redirect 不生效routes-manifest.jsonrewrites/redirects
client component 找不到<page>_client-reference-manifest.js 是否生成
middleware 不触发middleware-manifest.jsonmatchers 是否覆盖
edge route 跑成 nodejsfunctions-config-manifest.jsonruntime
standalone Cannot find module<page>.nft.json 是否缺这个文件;用 outputFileTracingIncludes
字体没 preloadnext-font-manifest.json 是否包含本 layout
ISR revalidate 失效prerender-manifest.jsoninitialRevalidateSeconds
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

十四、本讲小结

  1. .next/ 7 个一级目录:static / server / cache / standalone / types / trace + 顶层 manifest。
  2. 12 个核心 manifest 是 build 与 runtime 的契约;排障第一步是查 manifest。
  3. .nft.json 决定 standalone 复制哪些文件,缺文件主要找它。
  4. .next/cache/ 是 build 加速器,但偶尔需要清理。
  5. output: 'standalone' 是容器部署的最佳实践——自包含、小镜像、快冷启动。

阶段四完结 → 进入阶段五

至此第 23-28 讲(阶段四:构建、打包与优化)全部完成。下一讲进入阶段五:运行时与部署。

下讲预告:第 29 讲:Node Server / Standalone / Adapter 三种部署形态。我们会拆 next-start.ts、standalone server.js、Vercel adapter 的差异,以及 process 启动流程、port binding、graceful shutdown。