Next.js:App Router、Server Components 与显式缓存
最后核验:2026-06-30|适用版本:Next.js 14–16.2(示例锁定 16.2.9)|前置知识:React、HTTP 缓存、Node.js
Next.js 承担的全栈边界
React 定义组件与更新模型,Next.js 把路由、Server Components、构建、数据获取、缓存、流式响应和部署适配器组合成应用框架。需要服务端首屏、嵌套路由、前后端代码边界和统一生产构建时,这些固定能力可以减少集成工作;纯客户端单页或静态内容少量增强不必先承担整个框架。
App Router 是当前参照结果。Pages Router 仍有维护场景,但新文档不把两套路由混写;迁移时允许两者并存,逐路由验证请求和缓存行为。
App Router 的路由树
app/ 中的文件约定形成路由树:layout 保留共享界面,page 提供叶页面,loading 建立 Suspense fallback,error 建立客户端错误边界,Route Handler 处理 HTTP 请求。文件路径同时影响 URL、渲染边界和产物拆分。
嵌套布局不会因每次导航全部重建,但状态保留取决于 segment 身份和 key。需要页面级重置时改变路由边界,不用 Effect 观察路径后手工清空所有状态。
常见文件约定各有不同失败语义:
| 文件/目录 | 职责 | 易错点 |
|---|---|---|
layout.tsx | 跨子 segment 保留的共享布局 | 误以为每次导航都会重新取数和重置状态 |
template.tsx | 导航时重新创建的共享包装 | 无需求地使用会丢失子树状态并重复 Effect |
loading.tsx | 自动 Suspense fallback | 只做骨架,不应复制最终交互或产生布局跳变 |
error.tsx | segment 客户端错误边界 | 必须是 Client Component,不能捕获同级 layout 自身错误 |
not-found.tsx | 资源不存在视图 | 应由 notFound() 或未匹配路由触发,不等同任意 500 |
route.ts | Web Request/Response HTTP 端点 | 与同 segment 的 page.tsx 争用相同路由位置 |
(group) | 不进入 URL 的组织/布局边界 | 跨根 layout 导航可能触发完整页面加载 |
[id] / [...slug] | 动态和捕获 segment | 参数需验证,字符串类型不代表合法领域 ID |
page、layout 接收的 params 与 searchParams 在新版本是异步 Request API,要 await 后再使用。动态 segment 可以用 generateStaticParams 预生成已知页面,但它不自动保证未知参数 404,仍要明确动态策略与数据校验。元数据可用静态 metadata 或 generateMetadata 生成;其中取数应与页面共享去重/缓存策略,避免同一请求重复访问后端。
平行路由 @slot 在同一 layout 并列渲染独立子树,可分别拥有 loading/error 状态;拦截路由可在软导航时把详情放进模态框,在直接访问或刷新时展示完整页面。它们适合真实的 URL/历史语义,不是普通 tabs 的默认实现。设计时必须验证:直接打开、刷新、前进后退、关闭模态框、未知 slot 的 default.tsx 都有确定结果。
<Link> 会为可见链接预取后续导航所需 RSC 载荷,客户端 Router Cache 让共享 layout 和已取片段复用。预取不是业务数据永久缓存;大量动态链接、昂贵鉴权或计费接口要观察真实 Network,再决定是否关闭个别预取。编程导航使用 useRouter,可分享筛选条件进入 searchParams,不要把来自用户的完整 URL 直接传 router.push,否则可能形成开放跳转或 javascript: 注入。
Server Component 与 Client Component
App Router 组件默认是 Server Component,可访问服务端文件、数据库和密钥,但不能使用浏览器状态或事件。文件顶部的 'use client' 声明客户端模块边界,其传递依赖进入客户端图;不是每个使用该组件的地方都重新声明。
import catalog from '../../../contracts/technologies.json';
import Catalog from './catalog';
import { parseTechnologyEntries } from './model';
export default function Page() {
return <Catalog entries={parseTechnologyEntries(catalog as unknown)} />;
}传给 Client Component 的 props 必须可序列化。服务端函数、数据库连接和环境密钥不能进入 RSC 载荷;敏感信息即使最终被客户端隐藏,也已经越过信任边界。
'use client' 声明的是模块图入口。该文件直接或间接导入的运行时代码会进入客户端构建,所以把大型服务端工具、数据库模块或只为一个按钮需要的整页都放到边界下方,会扩大 bundle 甚至触发构建错误。合理做法是让 page/layout 保持 Server Component,只把最低交互叶子变成 Client Component:
// app/entries/[id]/page.tsx : Server Component
import { notFound } from 'next/navigation';
import FavoriteButton from './favorite-button';
import { getEntry } from '@/lib/entries';
export default async function EntryPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const entry = await getEntry(id);
if (!entry) notFound();
return (
<article>
<h1>{entry.title}</h1>
<FavoriteButton entryId={entry.id} initialValue={entry.favorite} />
</article>
);
}Client Component 仍会参与服务端首屏 HTML 预渲染;“Client”表示其代码也在浏览器运行,不等于只在浏览器渲染。Server Component 可以把 JSX 作为 children 传给 Client Component,使交互外壳不必导入整个服务端子树;反过来,Client Component 不能普通 import Server Component,因为那会把依赖拉入客户端图。
RSC props 支持 React 定义的可序列化值,而不是任意类实例。传输前应把 ORM entity、带 prototype 的领域对象和错误对象映射成最小 DTO。NEXT_PUBLIC_ 环境变量会内联到客户端代码,构建后无法靠运行时替换恢复秘密属性;服务端密钥只在服务端模块读取,并可用 server-only 阻止误导入。
请求、RSC 载荷与流式渲染
服务端先执行 Server Component,生成 React Server Component(RSC)载荷,并用它产生 HTML;客户端用载荷协调组件树并水合 Client Components。Suspense 边界允许静态壳和较慢数据分段到达。流式渲染改善等待呈现,不会让慢查询消失,仍需定位后端依赖和超时。
首屏水合要求服务端与客户端第一次渲染一致。直接在 Render 中读 window、当前时间或随机值会制造差异;浏览器专属逻辑进入 Client Component 的事件或 Effect。不要用 suppressHydrationWarning 掩盖数据边界错误。
首次请求与后续导航不是同一协议
首次请求中,服务端按 route segment 执行 Server Components,产生 RSC Payload;Next.js 再结合 Client Component 引用预渲染 HTML。浏览器先显示 HTML,再用 RSC Payload 对齐服务端/客户端组件树,最后下载 JavaScript 水合 Client Components。后续软导航通常直接请求并使用 RSC Payload,不需要整页 HTML,已有 layout 与客户端状态可保留。
RSC Payload 不是可忽略的内部日志。它包含服务端渲染结果、Client Component 模块引用和传给客户端的 props,能被浏览器请求者读取。鉴权必须在每次数据读取和 Server Function 执行前完成,不能因为某个组件“服务器执行”就假设结果保密。
Suspense 把树分成可独立完成的段。loading.tsx 为 segment 自动建立边界,也可手工用 <Suspense fallback={...}> 包住慢子树;静态壳先发出,动态洞随后流入。边界过大时一个慢查询阻塞整页,过碎时 fallback 闪烁、请求和错误状态难以理解。fallback 尺寸应接近最终内容,并把真正独立的数据请求并行启动,避免父组件 await 后子组件才开始的瀑布。
export default function DashboardPage() {
return (
<main>
<h1>控制台</h1>
<Suspense fallback={<SummarySkeleton />}>
<Summary />
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity />
</Suspense>
</main>
);
}流一旦开始发送,后续错误不一定还能改变 HTTP 状态码;SEO、监控和代理规则需要理解这个边界。断开客户端连接也不等于数据库查询或写事务自动取消,服务端操作仍要显式超时、幂等与资源释放。
Server Functions、表单与 Route Handlers
'use server' 让异步函数成为可从客户端引用的 Server Function;在 <form action={fn}> 场景通常称 Server Action。它减少手写端点与客户端状态胶水,但本质仍是可由不可信客户端触发的网络操作。参数、身份、权限、CSRF/Origin 策略、速率、事务和审计都必须在函数内部重新处理。
// app/entries/actions.ts
'use server';
import { updateTag } from 'next/cache';
import { redirect } from 'next/navigation';
import { requireUser } from '@/lib/auth';
import { entries } from '@/lib/db';
export async function renameEntry(formData: FormData) {
const user = await requireUser();
const id = String(formData.get('id') ?? '');
const title = String(formData.get('title') ?? '').trim();
if (!/^[a-z0-9-]+$/.test(id) || title.length < 1 || title.length > 80) {
throw new Error('Invalid entry');
}
await entries.renameOwned({ id, ownerId: user.id, title });
updateTag(`entry-${id}`);
updateTag(`entries-${user.id}`);
redirect(`/entries/${id}`);
}表单在 JavaScript 尚未加载时仍可提交,加载后 React/Next 会增强交互。客户端可用 useActionState 表示预期校验结果、useFormStatus 表示 pending;不要把数据库错误原样返回浏览器。按钮禁用只防误触,不能保证 exactly-once,支付/创建类写入需要幂等键或数据库约束。
Route Handler 用 Web 标准 Request/Response 实现 GET/POST 等方法,适合 webhook、公开 API、文件和非 React 客户端。它和 Server Action 不是安全等级不同的两种后端:两者都要验证输入和权限。Webhook 通常需读取原始 body 校验签名,缓存失效端点需验证 secret 并限制 tag/path,外部 URL 代理需防 SSRF、重定向和响应体过大。
Cache Components、缓存键与失效
Next.js 16 的 Cache Components 以显式 'use cache'、cacheLife 与 cacheTag 描述缓存;动态代码默认按请求执行。缓存设计必须回答作用域、键、寿命和失效,而不是只说“开启缓存”。Next.js 16
| 需求 | 最小机制 | 失效方式 | 主要风险 |
|---|---|---|---|
| 每请求个性化数据 | 不缓存 | 下一请求重算 | 后端延迟 |
| 可复用公开数据 | 'use cache' | 时间或 tag | 错误共享用户数据 |
| 写后刷新当前视图 | refresh/导航刷新 | 当前请求触发 | 误以为清除所有缓存 |
| 精确内容失效 | cache tag | revalidateTag | tag 过宽或漏标 |
CDN、浏览器和 Next.js 数据缓存是不同层。部署 Adapter 必须正确映射路由、缓存和失效语义;开发服务器行为不能作为生产证明。
Cache Components 开启后,动态代码默认按请求执行,缓存变为显式选择。'use cache' 可放在异步函数、组件或文件级;框架以构建 ID、函数标识和可序列化参数等构成缓存身份。请求期 cookies()、headers() 等动态值不应在共享缓存作用域里直接读取,首选在外层读取、提炼成真正影响结果的参数后传入;否则很容易把用户数据共享给其他请求,或把高基数身份塞爆缓存。
import { cacheLife, cacheTag } from 'next/cache';
export async function getPublicEntry(id: string) {
'use cache';
cacheLife('hours');
cacheTag('entries', `entry-${id}`);
return db.entry.findPublicById(id);
}缓存配置的三个时间不是同义词:stale 控制客户端 Router Cache 可直接使用结果的窗口,revalidate 控制服务端何时在请求触发下后台再生,expire 表示多久后必须阻塞等待新结果。选择它们前先写业务容忍度:用户能接受多旧、更新后是否必须立即看到、源站失败时能否继续服务旧值。
失效 API也有不同一致性:
| API | 可调用位置 | 语义 | 使用场景 |
|---|---|---|---|
updateTag(tag) | 仅 Server Action | 立即过期,下一次读取等待新值 | 写后读必须看到自己的修改 |
revalidateTag(tag, 'max') | Action、Route Handler | 先服务旧值,后台刷新 | CMS、公开目录等可短暂陈旧内容 |
revalidatePath(path) | 服务端写入边界 | 使路径相关缓存失效 | 不知道细粒度 tag 的路由更新 |
refresh() | 仅 Server Action | 刷新当前客户端 Router | 非缓存数据写后重取当前视图 |
router.refresh() | Client Component | 发起新 RSC 请求并合并结果 | 用户主动刷新;不等于清除服务端缓存 |
tag 应对应领域实体和查询集合。修改一篇文章通常同时失效 post-{id} 与 posts-{tenant};只失效详情会让列表标题陈旧,统一使用 posts 又可能把所有租户同时打回源站。缓存键和 tag 设计必须包含 tenant、locale、权限可见性、草稿/发布状态等所有影响结果的输入。
还要区分 React 请求内 memoization、Next.js 持久缓存、客户端 Router Cache、CDN Cache-Control 与浏览器 HTTP cache。它们的键、进程范围和失效入口不同。开发模式的重复渲染、热更新和缓存行为也与生产不同,验收至少包括生产构建后的冷请求、命中、写后读取、并发再生、源站失败以及多实例部署。
错误边界、取消与安全
error.tsx 捕获 segment 渲染错误并提供重试,not-found 表达资源不存在,Route Handler 返回明确 HTTP 状态。取消客户端请求使用 AbortSignal;服务端写入仍需幂等和事务,连接中断不保证服务器回滚。
React Server Components 在 2025 年出现远程代码执行与源码暴露漏洞。使用 App Router 的项目即使没有显式 Server Function 也必须升级到框架安全修复线,并持续核对 Next.js 公告。React RSC 安全公告
错误需要按层分类。error.tsx 能捕获其子 segment 渲染期异常并通过 reset() 重试,但不能替代 Route Handler 的状态码,也不应用于“表单标题太长”这类预期失败。预期失败返回类型化结果并在表单显示;真正异常记录内部关联 ID,向用户返回稳定信息。根级致命错误用 global-error.tsx,404 用 notFound();redirect/notFound 通过框架控制流实现,不要在广泛 try/catch 中误吞。
'use client';
export default function ErrorBoundary({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<section role="alert">
<h2>此区域暂时无法加载</h2>
<p>错误编号:{error.digest ?? 'unknown'}</p>
<button type="button" onClick={reset}>重试</button>
</section>
);
}安全审查围绕边界而非文件后缀展开:Server Component 返回值是否泄密;Action/Handler 是否重新鉴权;redirect 和图片/代理 URL 是否允许外部协议;富文本是否净化;缓存是否跨身份;日志是否含 cookie/token;middleware/proxy 是否只是优化而非唯一授权点。dangerouslySetInnerHTML、远程 MDX、服务端模板和 RSC 序列化分别需要对应的输入策略,不能用 TypeScript 类型替代运行时验证。
性能:优化服务器、网络和水合三条路径
Next.js 性能不能只看 Lighthouse 一个分数。首个文档涉及服务端等待与流式 HTML,RSC 请求涉及数据查询与 payload,Client Components 涉及下载/解析/水合,图片和字体又影响布局绘制。先用生产构建收集 Server-Timing、数据库 trace、Network waterfall、React Profiler 和浏览器 Performance。
- 并行启动独立数据请求,把 Suspense 边界放在真实延迟边界;删除串行 await 瀑布。
- 缓存只有在复用率和陈旧容忍度成立时启用;防止高基数键、stampede 和跨用户共享。
- 缩小
'use client'模块图,延迟非首屏交互,检查 bundle analyzer,而不是仅数 Client Component 个数。 - 为图片声明尺寸与响应式
sizes,字体控制子集和 preload;优化 LCP 元素而非全站统一 eager。 - 检查
<Link>预取是否制造后台流量,第三方脚本是否占主线程,RSC payload 是否传了客户端不用的大对象。
React Server Components 能减少客户端 JavaScript,却可能增加服务端 CPU、数据库压力与导航请求;Cache Components 能降低源站成本,却引入陈旧和失效复杂度。每项优化都要同时报告用户延迟、资源成本与正确性回归,不能只把工作从浏览器搬到服务器后宣布更快。
知识目录示例的运行证据
Next.js 示例在 Server Component 中导入并验证共享 JSON,只把八项安全领域数据传给一个 Client Component。查询、详情和对比在客户端完成,URL 用 History replace 语义更新。
pnpm --filter @roadmap/nextjs-slice test
pnpm --filter @roadmap/nextjs-slice preview --port 4205输入:访问 http://127.0.0.1:4205/,查看页面源代码,搜索 server,选择两项并刷新。
预期结果:模型测试与生产构建通过;HTML 首屏包含目录内容,客户端无水合警告,URL 恢复查询与选择。示例不启用缓存和 Server Action,因为只读本地 JSON 没有失效或写入问题。
调试与部署适配器
先区分错误发生在构建、Server Component、Route Handler、RSC 传输还是客户端水合。开发终端看服务端日志,浏览器 Network 看 HTML/RSC 请求,React DevTools 看客户端树。Next.js 16.2 提供更清楚的水合差异和 Server Function 日志;这些是定位工具,不替代确定性输入。Next.js 16.2
Next.js 16.2 的 Adapter API 已稳定,构建输出可由平台适配器映射到不同基础设施。选择部署平台前验证 Node 运行时、流、缓存、图片和失效支持范围,不把单一平台扩展写进领域代码。跨平台 Adapter
重大重构、严重安全修复与 Next.js 14 到 16 迁移
| 节点 | 重大重构 | 破坏性/高风险变化 | 回归重点 |
|---|---|---|---|
| 14 → 15 | App Router 成为主线,React 19 集成,缓存默认逐步转显式 | fetch/路由缓存旧假设、异步 Request API 迁移 | 动态/静态判定、缓存命中、cookies/headers |
| 15 → 16 | Cache Components + use cache、PPR 统一模型;Turbopack 默认生产构建 | 同步 params/searchParams/cookies() 等移除;webpack 插件不一定兼容 | codemod 后类型、构建、路由 shell、失效和 bundle |
| 16.2 | Adapter API 稳定;RSC 反序列化与 Turbopack 大量重构 | 平台必须实现公开 Adapter 契约;实验导航选项不可当稳定 API | 共享 adapter 测试、CDN 头、流、图片与 revalidation |
Next.js 的严重问题集中在“框架替应用处理协议”的位置。2025-12 的 CVE-2025-66478 跟踪 React RSC CVE-2025-55182 对 App Router 的下游影响,可导致未认证远程代码执行;后续还有 RSC 拒绝服务和源码泄露。2026-03 的请求走私 GHSA-ggv3-7p47-pfv8 影响把 rewrite 代理到外部后端的部署,修复线含 16.1.7 与 15.5.13。2026-05 官方又发布 middleware/proxy 绕过、Cache Components 连接耗尽、WebSocket SSRF、RSC cache poisoning 与 CSP nonce XSS 等公告。升级策略必须是“当前 LTS 最新安全 patch + React 对应最新 patch”,不能只满足某一个 CVE 的最低版本。Next.js Security Advisories、RSC 安全更新、请求走私公告
升级后用攻击路径而非首页烟测验收:大小写/编码变化能否绕过 middleware,恶意 Host/upgrade/rewrite 是否访问内部地址,Cache Components 在慢请求下是否耗尽连接,未登录 Server Function 是否拒绝,缓存错误是否跨用户复用。授权必须在数据与操作边界重新执行,middleware 只做早期拒绝;缓存键必须包含所有影响身份和内容的输入。
14–16 的关键变化集中在 App Router 稳定度、Turbopack、缓存语义和 React 版本。Next.js 16 默认 Turbopack,缓存更显式,并要求 Node.js 20.9+;当前 16.x 是 Active LTS,15.x 是 Maintenance LTS。支持策略
验收标准:能画出 Server/Client Component 边界、RSC 载荷和水合流程;为每个缓存写出键与失效;能解释部署 Adapter 是否保留这些语义。
Next.js 官方资料
- App Router:路由、渲染和数据获取入口;核验于 2026-06-30。
- Server and Client Components:模块边界与序列化;核验于 2026-06-30。
- Caching:Cache Components、生命周期与失效;核验于 2026-06-30。
- Deploying:运行模式与平台能力;核验于 2026-06-30。