Next.js 生产实践
适用于 Next.js 16 App Router 应用中的任何代码。安全相关规则(操作中的认证、CSP、标头、taint)在 nextjs-security 中;容器规则在 docker-nextjs 中。Claude Code 的内置 next-best-practices 技能涵盖了通用文件约定、元数据、图片和字体优化;本技能只补充我们构建和交付方式特有的内容。
基线:Next 16.4、React 19.3、Turbopack(dev 和 build 的默认值)、cacheComponents: true 且 partialPrefetching: true、output: "standalone"、typedRoutes: true。模板的 next.config.ts 是权威来源。
规则
-
默认使用 Server Components;仅在需要状态、副作用、浏览器 API 或事件处理程序时使用
"use client"。 将指令放在需要它的最小叶子组件上。不要因为文件导入了 nuvui 就添加它:nuvui 的有状态部分自带指令(参考nuvui技能中的rules/server-components.md)。// 错误:整个页面因为一个处理程序变成了客户端组件 "use client"; export default function Page() { … <Select onValueChange={…} /> … } // 正确:页面保留在服务端,处理程序位于叶子组件中 export default async function Page() { const people = await getPeople(); return <PeopleFilter people={people} />; // people-filter.tsx 有 "use client" }传递给客户端组件的 props 必须是可序列化的:不能包含函数(Server Actions 除外),不能包含类实例。
-
在需要数据的组件中的服务端进行数据获取。 不要使用
useEffect获取数据,也不要仅仅为了给自己页面提供数据而创建路由处理程序。在每个请求内使用 React 的cache()进行去重;跨请求缓存使用规则 3。 -
使用
"use cache"+cacheLife+cacheTag进行缓存;其他所有内容均为动态。 使用缓存组件时,除非明确指定,否则没有任何内容被缓存,并且fetch没有隐式缓存。import { cacheLife, cacheTag } from "next/cache"; export async function getInvoice(id: InvoiceId) { "use cache"; cacheLife("minutes"); cacheTag(`invoice:${id}`); return db.invoice.findUnique({ where: { id } }); }不要使用旧的段配置(
export const dynamic、revalidate、fetchCache):在缓存组件下会报错。参数和闭包值会成为缓存键的一部分,因此传递 ID,而不是整个请求对象。详细信息、配置文件和"use cache: private":请参考 references/caching.md。 -
在 Server Actions 中使用
updateTag进行失效处理,在其他地方使用revalidateTag(tag, "max")。updateTag(tag)实现了写后读一致性,且仅在 Server Action 内部有效。从 webhook 或路由处理程序中,使用revalidateTag(tag, "max")(stale-while-revalidate)或revalidateTag(tag, { expire: 0 })。单参数形式revalidateTag(tag)已废弃。 -
将请求时的数据包裹在
<Suspense>中,否则构建会失败。 在 Suspense 边界之外的cookies()、headers()、searchParams、connection()和未缓存的 I/O 会因“预渲染期间存在未缓存或运行时数据”而停止预渲染。为其提供一个包含骨架屏 fallback 的边界,缓存它,或者(仅根布局,见规则 14)使用export const instant = false标记路由为阻塞式。params、searchParams、cookies()、headers()都是异步的:使用await。 -
每个 Server Action 都必须自行验证输入并检查授权。 Server Action 是一个公共的 POST 端点,无论它写在哪个页面。使用
typescript-production中的 schema(valibot)解析FormData,然后授权,接着进行变更,最后调用updateTag。对于预期的失败,返回类型化结果;仅对错误抛出异常。完整规则在nextjs-security中。"use server"; export async function renameProject(_: State, form: FormData): Promise<State> { const session = await requireSession(); // 如果不存在则返回 401 const input = v.safeParse(RenameProject, Object.fromEntries(form)); if (!input.success) return { ok: false, errors: flatten(input.issues) }; await assertCanEdit(session, input.output.projectId); // 如果无权限则返回 403 await db.project.update(…); updateTag(`project:${input.output.projectId}`); return { ok: true }; } -
路由处理程序用于其他客户端(webhook、移动端、健康检查),而不是用于你自己的页面。 将它们放在
app/api/下。像 action 一样验证 body,返回Response.json(...),并有意设置状态码。 -
每个应用都有
GET /api/health,动态且无依赖。 DockerHEALTHCHECK和 Kubernetes 探针会访问它。它必须在不接触数据库的情况下响应,因此慢速依赖不会导致每个 pod 重启。await connection()防止它被预渲染为静态文件。它位于模板的src/app/api/health/route.ts;proxy.ts在其 matcher 中排除了/api/health。 -
在根目录以及用户体验不同的每个段放置
error.tsx、loading.tsx和not-found.tsx。error.tsx是一个客户端组件;通过instrumentation.ts的onRequestError记录日志,显示带有reset的重试按钮,绝不显示错误消息。对缺失的记录调用notFound(),而不是渲染空状态。 -
NEXT_PUBLIC_*在构建时被内联;其他所有内容在运行时于服务端读取。 同一个镜像从主环境无需修改即可提升到生产环境,因此NEXT_PUBLIC_值在每个环境中都是相同的。只在那里放置构建时常量(应用名称、一个各环境都相同的公共 Sentry DSN)。每个环境的配置(API URL、特性开关、密钥)是服务端的process.env.X,在 Server Component 或 action 中读取,并在需要时作为 props 传递给客户端。在src/env.ts中使用 valibot 一次性验证环境变量,并从那里导入。绝不要在next.config.ts中使用env键:它会被内联到客户端 bundle 中。 -
使用
import "server-only"标记仅服务端模块。 任何读取密钥、访问数据库或导入带有凭据的 SDK 的模块。客户端导入它会导致构建失败,而不是泄露信息。 -
使用
after()处理不应延迟响应的任务(审计日志、分析、缓存预热)。在调用after之前读取cookies()/headers(),而不是在它内部。它在同一进程中运行,因此 pod 需要其 SIGTERM 宽限期(chart 设置了terminationGracePeriodSeconds)。 -
使用
src/instrumentation.ts进行 Instrumentation:registerOTel+onRequestError。 通过@vercel/otel实现 OpenTelemetry,导出器由运行时标准的OTEL_EXPORTER_OTLP_*环境变量配置。参考 references/observability.md。 -
保持
output: "standalone",且绝不从工作区外部导入。 Docker runner 只复制.next/standalone、.next/static和public。在 monorepo 中,server.js位于.next/standalone/apps/<app>/server.js;工作区根目录根据pnpm-lock.yaml找到,因此除非构建警告,否则不要设置outputFileTracingRoot或turbopack.root。自托管详情(多副本、Server Action 密钥、deploymentId):参考 references/self-hosting.md。 -
根布局读取 CSP nonce,因此应用按每个请求渲染。 模板的
proxy.ts设置了基于 nonce 的 CSP,并且src/app/layout.tsx中有export const instant = false。这是一个刻意的权衡:nonce 和预渲染的静态 shell 无法共存,对于登录后的业务应用,严格的 CSP 更胜一筹。使用"use cache"的数据缓存仍然有效。在未阅读nextjs-security规则 4 之前,不要移除其中任何一半。 -
样式:全局 BEM SCSS,由组件导入。 Turbopack 使用
sass包编译 Sass;sassOptions.loadPaths: ["src/styles"]使得从任何深度都可以使用@use "abstracts"。不使用~导入,不使用sassOptions.functions。规则在scss-bem中。 -
图片通过
next/image,字体通过next/font。 Next 自带用于优化器的sharp;runner 镜像不需要额外包。远程图片需要带有精确主机名的images.remotePatterns。 -
使用
pnpm next analyze检查 bundle 大小(Turbopack 的分析器;@next/bundle-analyzer仅适用于 webpack)。使用next/dynamic懒加载重型客户端组件(nuvui 编辑器、地图、折叠线以下的图表)。 -
使用类型化路由和生成的类型。
typedRoutes: true检查<Link href>;PageProps<"/invoices/[id]">和LayoutProps在next typegen之后全局可用(由next dev/next build自动运行)。不要手动编写params类型。 -
不使用
next lint。 它在 16 版本中被移除了。Biome 负责 lint 检查(参见biome);TypeScript 使用每个包的tsc --noEmit进行检查。
验证
pnpm typecheck
pnpm lint
pnpm build # turbo run build:预渲染错误、类型错误、Sass 错误都会导致失败
pnpm --filter <app> start & # 然后:
curl -fsS localhost:3000/api/health
来源
- https://nextjs.org/docs/app/guides/upgrading/version-16
- https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents
- https://nextjs.org/docs/app/guides/migrating-to-cache-components
- https://nextjs.org/docs/app/api-reference/directives/use-cache
- https://nextjs.org/docs/app/api-reference/functions/revalidateTag
- https://nextjs.org/docs/app/api-reference/functions/updateTag
- https://nextjs.org/docs/messages/blocking-prerender-dynamic
- https://nextjs.org/docs/app/api-reference/functions/after
- https://nextjs.org/docs/app/guides/open-telemetry
- https://nextjs.org/docs/app/api-reference/config/next-config-js/output
- https://nextjs.org/docs/app/guides/self-hosting
- https://nextjs.org/docs/app/guides/environment-variables
- https://nextjs.org/docs/app/guides/sass
- https://nextjs.org/docs/app/guides/package-bundling
- https://nextjs.org/docs/app/api-reference/file-conventions/proxy