基于 Next.js 16.2 · App Router · React 19

Next.js 的架构、运行与数据流

这份文档不教你"怎么写一个页面",而是带你看清 Next.js 这台机器内部是怎么转的——它的架构怎么分层、一次请求在它体内如何流动、有哪些关键数据结构在背后支撑、数据又是如何从服务器一路串到屏幕上的。读完你应该能在面试里把"原理题"答出底气。

当前稳定版 16.2.x 默认打包器 Turbopack 缓存模型 Cache Components 中间件 proxy.ts React 19.2

00定位与心智模型


面试官问"Next.js 是什么",最差的答案是"一个 React 框架"。这句话对,但什么都没说。准确的定位是:

◆ 一句话定位

Next.js 是一个 建立在 React 之上的全栈应用框架,它把"服务端渲染、路由、数据获取、构建打包、部署运行时"这些原本要你自己拼装的能力,整合成一套以文件系统为约定、以 React Server Components 为渲染内核的一体化方案。它要解决的核心矛盾是:既要服务端渲染带来的首屏速度与 SEO,又要单页应用的交互体验,还要让开发者写起来像写普通 React。

三个层次理解它

要真正"懂"Next.js,得在脑子里分清它同时扮演的三个角色。这三层贯穿全文,建议先记住:

① 框架层 / Framework
文件系统路由、布局嵌套、约定式文件(page/layout/loading…)、配置体系。这是你直接打交道的"开发者界面"。
② 渲染层 / React + RSC
React Server Components、Suspense、Streaming、Hydration。这是决定"什么在服务端跑、什么在客户端跑、HTML 怎么生成"的内核。
③ 运行时与工具链 / Runtime & Toolchain
Turbopack(打包)、SWC(编译)、Node.js / Edge 运行时、缓存系统、部署适配器。这是把代码变成可运行产物、并在生产环境里跑起来的底座。
图 0 · Next.js 的三层心智模型——全文都会回到这张图
★ 面试高频

"Next.js 相比纯 React(CRA/Vite)多了什么?" 答题骨架:① 服务端渲染能力(SSR/SSG/ISR/PPR),首屏更快、利于 SEO;② 约定式路由与嵌套布局,省去手动配置 React Router;③ React Server Components,让一部分组件在服务端运行、零 JS 下发;④ 内建数据获取与缓存模型;⑤ 内建优化(图片、字体、脚本、代码分割);⑥ 全栈能力(Route Handlers / Server Actions 让前后端同仓);⑦ 开箱即用的构建与部署优化。一句话收尾:它把"渲染策略 + 路由 + 数据 + 构建 + 运行时"打包成了一个有明确约定的整体。

版本演进:为什么现在长这样

理解架构必须知道它"从哪来"。这条演进线本身就是高频面试题:

阶段关键变化背后的架构动机
Pages Router
(经典时代)
pages/ 目录、getServerSideProps / getStaticProps / getInitialProps 三件套页面级的渲染策略,数据获取与组件分离,整页要么静态要么动态,粒度粗。
App Router
(13 引入 / 现默认)
app/ 目录、React Server Components、嵌套布局、fetch 直接在组件里写、Streaming把渲染粒度下沉到组件级:同一页面里可以混合服务端与客户端组件、静态与动态片段。这是范式转变。
14 / 15Server Actions 稳定、Turbopack 逐步成熟、缓存默认行为调整、请求 API 异步化补齐数据"写"路径(Actions),并开始反思"默认全缓存"带来的心智负担。
16
(当前)
Turbopack 成为默认;middleware.tsproxy.ts(Node 运行时);Cache Components 新缓存模型;同步请求 API 移除缓存从"隐式默认缓存"翻转为"默认动态、显式 use cache 才缓存",让缓存可预测;PPR 成为常态化能力。
▲ 常见误区

很多人把 App Router 当成"换了个目录名的 Pages Router"。不是。 App Router 的内核是 RSC,渲染默认发生在服务端、组件默认零 JS;Pages Router 里所有组件都是客户端组件(在服务端只是被渲染成 HTML 字符串再水合)。两者的数据获取、缓存、组件能力都不一样。本文以 App Router 为主线,必要处对照 Pages Router。

01整体架构:一次请求看穿全貌


先建立"上帝视角"。下面这张图是 Next.js 处理一个页面请求时,各个子系统的协作关系。后面每一章,本质上都是在放大其中某一块。

CLIENT / 浏览器 用户请求 / 导航 URL · Link · router HTML + RSC Payload 首屏直出 → 水合 Router Cache 缓存 交互后再请求 PROXY / 边缘 proxy.ts 重写/重定向 鉴权/改 header SERVER / Node · Edge 运行时 路由匹配 Route Tree · Segments RSC 渲染 Server Components → RSC Payload SSR → HTML Streaming + Suspense 静态壳先发 数据层 fetch / DB / use cache Server Actions 缓存系统 Request Memo use cache / cacheLife cacheTag ───────── revalidateTag revalidatePath updateTag
图 1 · 一次页面请求在 Next.js 各子系统间的流动

把这张图拆开,本文的章节其实就是沿着这条流水线展开的:路由匹配(第 5 章)→ RSC 渲染(第 2 章)→ SSR 流水线 + Streaming(第 7、8 章)→ 数据与缓存(第 10、11 章)→ 回到客户端的 水合与导航(第 9 章)。而第 13、14 章专门讲流水线里流动的"数据结构"是什么样子。

关键架构原则:服务端优先,按需下沉到客户端

App Router 的所有设计都围绕一条原则:默认在服务端做尽可能多的事,只把必须交互的部分下放到客户端。 这条原则带来三个直接结果——默认更少的客户端 JS、组件可以直接访问后端资源、数据获取就在组件内联完成。理解了这条原则,后面 RSC、边界、缓存的种种约定就都顺理成章了。

02React Server Components:渲染内核


RSC 是 App Router 的地基。如果只能记住一个概念,就是它。

它到底是什么

React Server Components 是一类只在服务端运行、永远不会被打包进客户端 JS 的组件。 在 App Router 里,组件默认就是 Server Component,除非你显式声明 "use client"

◆ 类比

把页面想象成一栋房子。Server Component 是预制好的墙体和家具——在工厂(服务端)就组装好,运到现场已经是成品,不需要现场施工(不下发 JS、不在浏览器执行)。Client Component 是需要通电的电器——必须把它的"电路图"(JS bundle)也运到现场,插上电(水合)才能用(响应点击、维持状态)。一栋房子里大部分是墙和家具,只有少数地方需要通电。

Server Component 能做与不能做

Server Component(默认)Client Component("use client"
运行位置仅服务端(构建时或请求时)服务端预渲染一次 + 浏览器水合后运行
下发 JS❌ 零客户端 JS✅ 组件代码打包进客户端
直接访问后端✅ 数据库、文件系统、密钥❌ 只能通过 API / Action
async/await✅ 组件本身可以是 async❌ 不能(用 use / hooks)
state / effect❌ 不能用 useState/useEffect✅ 完整 hooks
事件处理❌ 不能 onClick✅ 可交互
浏览器 API❌ 无 window/localStorage✅ 有
app/posts/page.tsx — Server Component(默认)
// 没有 "use client",这就是一个服务端组件 // 注意它是 async 的,可以直接 await 数据 —— 这是 RSC 才有的能力 export default async function PostsPage() { // 直接读数据库 / 调内部服务,代码不会下发到浏览器 const posts = await db.post.findMany({ orderBy: { createdAt: 'desc' } }) return ( <ul> {posts.map((p) => ( <li key={p.id}>{p.title}</li> ))} </ul> ) }
★ 面试高频

"Server Component 为什么能直接 await 数据,而普通 React 组件不行?" 因为 RSC 在服务端被渲染成一段序列化的描述(RSC Payload),而不是立即生成 DOM;服务端渲染本身就是一个可以等待的异步过程,React 在服务端会等 Promise resolve 后再把结果写进 payload。客户端组件要参与浏览器的同步水合,无法在渲染期挂起等待网络(要异步数据得用 use() 配合 Suspense 或 effect)。

RSC 的输出不是 HTML,而是"RSC Payload"

这是最容易答错的点。Server Component 渲染的产物不是直接的 HTML 字符串,而是一种 React 专用的序列化格式,常被称为 RSC PayloadFlight 数据。它本质上是一棵被序列化的 React 元素树,里面包含:

  • Server Component 渲染出来的结果(已经"算好"的元素树);
  • 遇到 Client Component 的地方,不内联代码,而是放一个模块引用(reference)——告诉客户端"这里要挂载哪个 chunk 里的哪个组件";
  • 从服务端传给 Client Component 的 props(必须可序列化);
  • Suspense 边界的占位与后续补发的分块(用于 Streaming)。

这个 payload 有两个用途:① 服务端用它生成初始 HTML(首屏);② 同时把 payload 本身也发给客户端,客户端据此水合,并在后续导航时复用。第 13 章会详细拆 payload 的结构。

▲ 易错点

"RSC = 服务端渲染 SSR" 是错的。RSC 是组件模型,SSR 是渲染过程,二者正交。 RSC 决定"哪些组件在服务端运行、产出 payload";SSR 决定"把这棵树转成 HTML 字符串发给浏览器"。App Router 同时用了两者:先 RSC 产出 payload,再 SSR 把它转成 HTML。一个 RSC 应用也可以完全静态化(构建时就跑完 RSC)。

03Server / Client 边界


整棵组件树被 "use client" 切成两个世界。理解"边界画在哪、props 怎么穿过去"是 App Router 工程实践的核心,也是面试常考的"原理 + 实践"结合题。

"use client" 是一道传染性的边界

"use client" 写在文件顶部,含义是:从这个模块开始,它以及它 import 进来的所有模块,都进入客户端世界。 它标记的是"进入客户端的入口点",而不是"只有这个组件是客户端"。

组件树 / 边界示意 RootLayoutserver Pageserver · async PostListserver · 直接读 DB "use client" LikeButton client · useState + import 的子模块 也全部进客户端 绿=服务端组件(零 JS) 红=客户端孤岛(下发 JS + 水合)
图 2 · 服务端组件构成树干,客户端组件是其中的"交互孤岛"

关键规则:Server 可以包 Client,Client 不能直接 import Server

数据流方向决定了组合方式。下面这三条是必背规则:

  1. Server Component 可以渲染 Client Component(在树里把客户端组件当子节点)。
  2. Client Component 不能直接 import 一个 Server Component——因为 Client 的代码要打包进浏览器,而 Server Component 本不该出现在浏览器。
  3. 但可以通过 children / props 把 Server Component "插槽"进 Client Component。这是绕过上一条限制的标准姿势。
把 Server Component 作为 children 传进 Client Component
// ✅ 正确:服务端组件作为 children 注入客户端组件 // app/page.tsx (Server) import ClientWrapper from './client-wrapper' // client import ServerData from './server-data' // server export default function Page() { return ( <ClientWrapper> {/* ServerData 在服务端渲染好,作为 RSC payload 的一部分 */} {/* 被"投影"进客户端组件的插槽里,客户端拿到的是渲染结果而非源码 */} <ServerData /> </ClientWrapper> ) }
★ 面试高频

"为什么 Client Component 不能 import Server Component,但能用 children 传进来?" 因为 import构建期的静态依赖,会把被引模块打包进客户端 bundle——这会让服务端组件的代码(含密钥、DB 访问)泄漏到浏览器。而 children/props 是运行期的数据流:服务端先把 Server Component 渲染成 payload,再把"渲染结果"作为一个已序列化的节点传给客户端组件,客户端拿到的只是结果,不是源码,所以安全。

▲ 易错点:传给 Client 的 props 必须可序列化

跨越边界传给客户端组件的 props 要能被序列化(写进 RSC payload)。函数、类实例、Symbol、Date 之外的复杂对象不能直接传(Server Actions 是被特殊处理的例外)。所以你不能把一个数据库连接、一个回调函数随手传过边界。这也是为什么"事件处理函数不能从 Server Component 传给 DOM"。

实践:把 "use client" 推到叶子节点

性能上的最佳实践是让边界尽量靠近叶子。一个常见反模式是在根布局上写 "use client",结果整棵树都变成客户端,RSC 的优势荡然无存。正确做法:只把真正需要交互的那一小块(按钮、表单输入、带动画的卡片)标成客户端,其余保持服务端。

04编译与构建架构


这一章回答"你的代码是怎么变成能跑的东西的"。面试里属于加分项——能讲清工具链说明你懂底层。

工具链分工

工具语言职责
TurbopackRust当前默认打包器(bundler),负责模块解析、依赖图、代码分割、HMR。取代了 webpack。Next 16 起 devbuild 默认走 Turbopack。
SWCRust编译器(compiler),负责 TS/JSX 转译、语法降级、各种 Next 专属转换(如标记 Server/Client 边界、Server Actions 转换)。取代了 Babel。
React Compiler1.0 已稳定(可选开启)。自动做 memo 化,减少手写 useMemo/useCallback

核心机制:一份代码,两份产物

这是构建阶段最关键的认知。因为有 Server/Client 之分,构建器会从你的代码生成两套 bundle

你的源码
app/**/*.tsx
Server Graph
在 Node/Edge 运行
服务端产物
.next/server
Client Graph
从"use client"入口起
客户端产物
.next/static/chunks
图 3 · 构建器从同一份源码切出 Server / Client 两张依赖图,产出两套 bundle

构建器以 "use client" 标记的模块为分界:边界以上(含 Server Components)进入服务端图,不打包给浏览器;边界以下进入客户端图,做代码分割后输出到 static/chunks。RSC payload 里对 Client Component 的"模块引用",指向的就是客户端图里对应的 chunk。

next build 都做了什么

  • 编译:SWC 把所有 TS/JSX 转译成可执行 JS。
  • 打包与分割:Turbopack 构建依赖图,按路由做代码分割(每个路由只加载自己需要的 chunk)。
  • 预渲染(prerender):对可静态化的路由,在构建时就跑完 RSC + SSR,产出静态 .html 和对应的 .rsc(RSC payload 快照)。
  • 生成清单(manifests):把"哪个路由对应哪些文件、哪些是静态/动态、入口 chunk 是什么"等元信息写成一堆 JSON(第 14 章详解)。
  • 优化:tree-shaking、minify、图片/字体处理等。

开发模式(next dev)和生产(next build && next start)行为不同:开发模式很多东西按需编译、缓存行为也更宽松。调试缓存/渲染问题,永远以生产构建为准。 这是实战里反复踩的坑,也值得在面试里点一句。

05路由系统:文件即路由


Next.js 用文件系统约定来表达路由——目录结构就是路由结构,特殊文件名承担特殊角色。这是"开发者界面"层最直观的部分,也是数据结构层"路由树"的来源。

约定式特殊文件

app/ 下,每个目录代表一个路由段(route segment),目录里的特殊文件决定这一段的行为:

文件作用关键特性
page.tsx该路由的页面 UI,使路由可被访问有它路由才"存在"。默认 Server Component。
layout.tsx共享布局,包裹子路由导航时不重新挂载、保留状态;可嵌套。必须渲染 children
template.tsx类似 layout,但每次导航重新挂载需要"进入即重置"(如埋点、动画)时用。
loading.tsx该段的加载态本质是自动包了一层 <Suspense>,配合 Streaming。
error.tsx该段的错误边界必须是 Client Component;捕获子树渲染错误。
not-found.tsx404 UInotFound() 触发。
route.tsAPI 端点(Route Handler)导出 GET/POST/...,处理 RequestResponse。与 page 互斥。
default.tsx平行路由的兜底 UI配合 @slot 使用。
global-error.tsx根级错误边界兜住整个应用的渲染错误。

这些文件如何"组装"成一个页面

关键认知:同一路由的特殊文件会按固定顺序嵌套包裹起来。 Next.js 渲染一个路由时,自动生成这样一棵包裹树:

特殊文件的嵌套顺序
<Layout> <Template> <ErrorBoundary fallback={<Error />}> <Suspense fallback={<Loading />}> <ErrorBoundary fallback={<NotFound />}> <Page /> </ErrorBoundary> </Suspense> </ErrorBoundary> </Template> </Layout>
★ 面试高频

"layout 和 template 的区别?" 两者都包裹子内容,但 layout 在导航中持久化——切换兄弟路由时 layout 实例及其内部状态(如展开的侧边栏、滚动位置)保留;template 每次导航都重新创建实例,状态清空、effect 重跑。需要"跨页面保持"用 layout,需要"每次进入都重来"用 template。

嵌套布局:路由树天然分层

目录嵌套天然形成布局嵌套。app/layout.tsx 包裹一切,app/dashboard/layout.tsx 只包裹 dashboard 子树。导航到 /dashboard/settings 时,只有变化的最深层段会重新渲染,外层布局原地不动——这是性能与体验的关键。

RootLayout · app/layout.tsx 导航中保持不变 ▲ DashboardLayout · app/dashboard/layout.tsx 侧边栏状态保留 ▲ SettingsPage /dashboard/settings ← 切到 /dashboard/billing 只有这块替换 BillingPage /dashboard/billing 新段,仅请求它的 RSC payload
图 4 · 嵌套布局:导航只替换变化的最深段,外层布局与状态原地保留

动态路由与高级路由约定

约定含义例子
[id]动态段,匹配单段app/post/[id]/post/42
[...slug]捕获所有后续段/docs/a/b/c
[[...slug]]可选捕获(含空)匹配 /docs 也匹配 /docs/a
(group)路由组:组织目录但不进入 URL(marketing)/about/about
@slot平行路由:同一布局里渲染多个独立子树仪表盘里 @team@analytics 并存
(.)folder拦截路由:用当前布局拦截另一路由(如模态框)列表里点图片弹模态而非整页跳转
_folder私有目录:不参与路由放工具/组件
app/blog/[slug]/page.tsx — 动态路由读参数(注意 params 现在是 async)
// Next 16:params / searchParams 都是 Promise,必须 await export default async function BlogPost({ params, }: { params: Promise<{ slug: string }> }) { const { slug } = await params const post = await getPost(slug) return <article>{post.title}</article> } // 静态预渲染:告诉构建器要为哪些 slug 生成静态页 export async function generateStaticParams() { const posts = await getAllPosts() return posts.map((p) => ({ slug: p.slug })) }
▲ 升级要点(Next 16)

paramssearchParams 以及 cookies()headers()draftMode() 在 16 里全部异步化,必须 await。同步用法在 16 已被移除。面试若被问"Next 15→16 有哪些破坏性变更",这是必答点之一(另两个是 Turbopack 默认、middleware.tsproxy.ts)。

路由系统在内存里:路由树(Route Tree)

所有这些文件约定,最终被 Next.js 解析成一棵路由树。每个节点是一个 route segment,携带该段的 layout / page / loading / error 引用、是否动态、参数名等。这棵树既用于服务端匹配请求,也用于客户端导航时定位"哪一段变了"。它的数据结构形态见第 13 章。

06渲染策略全景:SSG / SSR / ISR / CSR / PPR


这是 Next.js 面试的"必考大题"。先把五种策略一次性摆清楚,再讲它们在 App Router 里如何统一到一个模型下。

策略HTML 何时生成适用场景权衡
SSG
静态生成
构建时一次性生成博客、文档、营销页等内容不常变最快、可走 CDN;但内容更新要重新构建
ISR
增量静态再生
构建时生成 + 按需/定时在后台再生电商商品页、列表,量大且更新有节奏兼顾静态速度与新鲜度;首个过期请求可能拿到旧值(stale-while-revalidate)
SSR
服务端渲染
每次请求时生成个性化、强实时(用户面板、搜索结果)总是最新;但每请求都要算,TTFB 受服务端速度影响
CSR
客户端渲染
浏览器里用 JS 生成纯交互、登录后内部工具、无需 SEO 的部分首屏需等 JS;交互后体验好
PPR
部分预渲染
静态壳构建时 + 动态洞请求时同一页面里既有静态框架又有个性化片段兼得:壳秒开走 CDN,动态部分流式补上(Next 16 默认能力)
◆ 类比

把渲染想成"出餐":SSG 是预制菜,开门前全做好,谁来都直接端(最快但样式固定);SSR 是现点现做,每位客人都新鲜炒(最对口味但要等);ISR 是热柜里的预制菜,卖光或过一段时间就补一锅新的;CSR 是给你一套食材和说明书,回家自己炒;PPR 是预制好餐盘和摆盘(静态壳),只把那道需要现做的主菜(动态洞)现炒后补上桌。

App Router 如何把这些统一起来

在经典 Pages Router 里,渲染策略是页面级的(由你导出 getStaticProps 还是 getServerSideProps 决定)。App Router 把粒度下沉到组件 / 数据级,并在 Next 16 的 Cache Components 模型下统一成一句话原则:

★ 核心原则(Next 16)

默认动态(dynamic by default),显式缓存(use cache)才静态。 一个组件/函数有没有标 use cache、读不读请求特定数据(cookies/headers/动态 params),共同决定了它是被预渲染进静态壳,还是在请求时动态计算。整页不再是"非静态即动态",而是一页之内静态与动态并存——这正是 PPR。

具体而言,决定某段是静态还是动态的因素:

  • 标了 use cache(配合 cacheLife)→ 可被预渲染、缓存复用 → 走静态/ISR 语义。
  • 读取了请求特定数据(cookies() / headers() / 动态 searchParams)→ 只能请求时算 → 动态。
  • 未缓存的数据访问被 <Suspense> 包裹 → 成为"动态洞",壳先发、它流式补上 → PPR。
▲ 新旧模型对照(重要)

Next 16 之前是"默认缓存"fetch 默认被缓存、整页倾向静态,靠 export const dynamic = 'force-dynamic' / revalidate 等路由段配置来"退出缓存"。Next 16 启用 cacheComponents: true 后翻转为"默认动态",靠 use cache 主动"进入缓存"。心智从"我要怎么关掉缓存"变成"我要给什么加缓存",更可预测。注意:不开 cacheComponents 时,旧的 export const revalidate 等仍可用,但官方建议迁移到 use cache

07SSR 渲染流水线:从请求到屏幕


把第 6 章的"策略"落到"过程"。一次动态请求,HTML 是怎么一步步出现在用户屏幕上的?这是把 RSC、SSR、Streaming、Hydration 串起来的关键一章。

1请求到达 · 路由匹配在路由树中定位 segments,确定要渲染的组件 2服务端运行 RSCServer Components 执行 · await 数据产出 RSC Payload(含 Client 组件引用) 3SSR:payload → HTMLReact 把 payload 转成 HTML 字符串遇 Suspense 边界先发 fallback 4流式传输 StreamingHTML 分块发送,静态壳先到数据 ready 后补发对应 chunk 5浏览器:边收边画首屏 6下载 Client chunks按 payload 里的模块引用拉取交互组件的 JS 7水合 HydrationReact 用 RSC payload + 已有 HTML给客户端组件"接上事件/状态"不重绘 DOM,只附加交互 8页面完全可交互后续导航走客户端软导航 HTML+payload
图 5 · 一次动态请求的完整流水线:服务端 RSC→SSR→Streaming(1-5)与客户端下载→水合(6-8)

逐步拆解

① 路由匹配。 服务端拿到 URL,在路由树里定位命中的 segments,组装出第 5 章那棵"layout→…→page"的包裹树。

② 运行 RSC。 服务端执行所有 Server Components。async 组件在这里 await 数据。产物是 RSC Payload——元素树 + Client 组件的模块引用 + 序列化 props。

③ SSR 转 HTML。 React 把 payload 转成 HTML 字符串。这一步让首屏不依赖 JS 就能显示,对 SEO 和首屏速度至关重要。

④ Streaming。 关键优化:HTML 不是等全部算完才发,而是边算边发。能立即确定的"静态壳"先发出去;被 <Suspense>(或 loading.tsx)包裹、还在等数据的部分先发 fallback 占位,数据 ready 后再把真实内容作为后续 chunk 补发,浏览器原地替换占位。

⑤–⑧ 客户端接管。 浏览器边收边渲染首屏(用户很快看到内容);同时按 payload 里的模块引用下载交互组件的 JS chunk;然后进行水合——把已有的静态 HTML"激活",给客户端组件接上事件监听与状态,不重新创建 DOM。水合完成,页面完全可交互。

★ 面试高频

"什么是 Hydration?为什么会有 hydration mismatch?" 水合是 React 在客户端"复用服务端已渲染的 HTML"、把事件与状态附加上去的过程,避免重绘。mismatch 发生在服务端渲染出的 HTML 与客户端首次渲染结果不一致时——常见原因:用了 Date.now()/Math.random()/window 等服务端客户端不一致的值,或把只在浏览器存在的状态用在了首渲染里。解决:把这类逻辑放进 useEffect、用 suppressHydrationWarning、或确保两端输入一致。

◆ 类比 Streaming + Hydration

Streaming 像上菜不等齐:先上能上的(静态壳),慢的菜(动态数据)做好了再端上来,客人不用干等整桌。Hydration 像给一栋已盖好的样板房通电:墙、家具(HTML)已经在了,电工只是把电线接上、开关装好(附加事件与状态),而不是把房子推倒重盖。

08PPR:部分预渲染(静态壳 + 动态洞)


PPR 是把第 6、7 章的能力推到极致的产物,也是 Next 现代架构的"招牌"。Next 16 通过 cacheComponents 让它成为常态行为。

核心思想:一页 = 一个静态壳 + 若干动态洞

传统模型里一个页面要么整页静态、要么整页动态。PPR 打破这点:把页面里能静态确定的部分(导航栏、布局、产品标题)在构建时预渲染成静态壳,可直接走 CDN 秒开;把个性化/实时的部分(购物车数量、推荐、库存)留作"动态洞",用 <Suspense> 包起来,在请求时流式填充。

静态壳 · 导航栏 / 页头(构建时预渲染,走 CDN) prerendered shell · 立即可见 静态:产品标题 / 描述 / 图片 use cache · 预渲染 动态洞:库存 / 个性化推荐 / 购物车 <Suspense> 包裹 · 请求时流式补上 先显 fallback 骨架 数据 ready → 替换 静态壳 · 页脚
图 6 · PPR:绿色静态壳构建时就绪、秒开走 CDN,红色动态洞请求时流式填充
next.config.ts — 开启 Cache Components(含 PPR 行为)
import type { NextConfig } from 'next' const nextConfig: NextConfig = { cacheComponents: true, // 启用:默认动态 + use cache 才静态 + PPR 默认 } export default nextConfig
app/product/[id]/page.tsx — 同页混合静态与动态
import { Suspense } from 'react' // 静态片段:标了 use cache,可预渲染进壳 async function ProductInfo({ id }: { id: string }) { 'use cache' const product = await getProduct(id) // 缓存复用 return <h1>{product.name}</h1> } // 动态片段:读实时库存,无缓存 → 成为动态洞 async function LiveStock({ id }: { id: string }) { const stock = await getStock(id) // 每次请求真实查询 return <span>剩余 {stock} 件</span> } export default async function Page({ params, }: { params: Promise<{ id: string }> }) { const { id } = await params return ( <div> {/* 静态壳的一部分,秒出 */} <ProductInfo id={id} /> {/* 动态洞:壳先发,骨架占位,数据好了再流式替换 */} <Suspense fallback={<StockSkeleton />}> <LiveStock id={id} /> </Suspense> </div> ) }
★ 面试高频

"PPR 和 ISR/SSR 的关系?" 可以这样收束:ISR/SSR/SSG 是页面级的渲染时机选择,而 PPR 是把它们融合进同一页面——壳走"构建时静态(≈SSG)"并可走 CDN,洞走"请求时动态(≈SSR)",靠 Streaming 把两者拼起来。它的前提是 RSC + Suspense + 流式渲染。一句话:PPR 让"静态的快"与"动态的新"在一个页面里共存。

10数据获取与缓存模型


这是 Next.js 区别于纯前端框架的灵魂,也是面试最能拉开差距的部分。我们分"读"(本章)与"写"(下一章 Server Actions)两条线。

读路径:在组件里直接取数据

App Router 的数据获取哲学是"数据获取下沉到需要它的组件,且优先在服务端完成"。Server Component 是 async 的,直接 await 即可——不需要 getServerSideProps 那种页面级钩子。

服务端组件内联取数(最常见)
async function UserProfile({ id }: { id: string }) { // 直接调数据库 / 内部服务 / fetch,代码不下发浏览器 const user = await db.user.findUnique({ where: { id } }) return <h2>{user.name}</h2> }

四类缓存:先认清各自管什么

"缓存"在 Next.js 里不是一个东西,而是分布在不同层、生命周期不同的几套机制。这是面试的"深水区",务必分清:

缓存位置 / 生命周期缓存什么怎么控制
Request Memoization
请求记忆化
服务端 · 单次渲染内同一次渲染中相同的 fetch/cache() 调用去重,只发一次React 自动;React.cache() 包裹自定义函数
Data Cache
(经典模型)
服务端 · 跨请求持久数据获取结果,可定时/按需再验证旧模型 fetch 选项 / revalidate;新模型由 use cache 取代
Full Route Cache
(经典模型)
服务端 · 构建/再验证时静态路由的 HTML + RSC payload路由是否静态决定;新模型由 Cache Components 统管
Router Cache
客户端路由缓存
客户端内存 · 会话内已访问/预取路由的 RSC payloadprefetchrouter.refresh()revalidate*

Next 16 的新缓存模型:Cache Components

Next 16 用 Cache Components 重塑了上面的"服务端持久缓存"那部分。核心是三个新原语,配合 cacheComponents: true 使用:

原语作用
'use cache'放在文件顶部或函数/组件内首行,把这个文件的导出 / 这个函数 / 这个 async 组件标记为可缓存。它的参数与 props 自动成为缓存键
cacheLife(profile)设定缓存的生命周期。内置 profile:seconds/minutes/hours/days/weeks/max,也可自定义 { stale, revalidate, expire }。语义是 stale-while-revalidate。
cacheTag(tag)给缓存条目打标签,便于按标签精准失效
use cache + cacheLife + cacheTag 三件套
import { cacheLife, cacheTag } from 'next/cache' async function ProductCard({ id }: { id: string }) { 'use cache' // 标记可缓存 cacheLife('hours') // 缓存按"小时"档过期/再验证 cacheTag(`product-${id}`) // 打标签,便于精准失效 const product = await db.product.findById(id) return <ProductDetail product={product} /> // id 作为入参,自动成为缓存键的一部分: // 不同 id 命中不同缓存条目 }

失效:让缓存"过时"

缓存的另一半是"如何让它失效"。数据变更(通常在 Server Action 里)后调用:

  • revalidateTag('product-42') —— 失效所有打了该标签的缓存条目(精准、推荐)。
  • revalidatePath('/products') —— 失效某路径相关缓存。
  • updateTag('...') —— 较新 API,在当前这次变更后立即让对应内容在本次渲染中更新(更强的即时一致性)。
★ 面试高频

"Next 16 缓存模型相比以前最大的变化是什么?"隐式默认缓存翻转为显式按需缓存:以前 fetch 默认被缓存、整页倾向静态,开发者要不断"退出缓存"且容易踩坑;现在默认动态,靠 use cache/cacheLife/cacheTag 主动声明哪些可缓存、缓存多久、怎么失效。配合 PPR,缓存粒度精确到组件/函数级,行为更可预测。补一句:客户端的 SWR/React Query 解决的是另一层问题(客户端缓存、聚焦重取、乐观更新),与 use cache(服务端)不冲突。

▲ 易错点

use cache 的作用域里不能读请求特定数据cookies()/headers()),因为那是 per-request 的、无法被多请求共享的缓存复用。带鉴权的页面别整页 use cache——应让页面保持动态,只缓存其中与用户无关的数据函数;或使用私有缓存语义。

11Server Actions:数据变更(写路径)


读路径之外,App Router 用 Server Actions 统一了"写"。它让你在客户端直接调用一个运行在服务端的函数,无需手写 API 路由。

本质:编译器生成的安全 RPC

"use server" 标记的函数,会被编译器处理成一个可从客户端安全调用的服务端端点。客户端调用它时,实际是发起一个 POST 请求,参数被序列化送到服务端执行,结果(或副作用)再返回。你写起来像调一个本地异步函数,底层是 RPC。

app/actions.ts — 定义 Server Action
'use server' // 整个文件的导出都是 Server Action import { revalidateTag } from 'next/cache' import { redirect } from 'next/navigation' export async function createPost(formData: FormData) { const title = formData.get('title') as string // 直接在服务端写库(安全,代码不下发) await db.post.create({ data: { title } }) // 写完让相关缓存失效,UI 拿到新数据 revalidateTag('posts') redirect('/posts') // 也可重定向 }
在表单中使用 — 支持渐进增强(无 JS 也能提交)
import { createPost } from './actions' export default function NewPost() { return ( // 把 action 直接绑到 form,提交即调用服务端函数 <form action={createPost}> <input name="title" /> <button type="submit">发布</button> </form> ) }
◆ 类比

Server Action 像店里的服务铃:你(客户端)按一下铃并说出需求(序列化参数),后厨(服务端)真正去做事(写库、改状态),做完顺手把菜单上的"今日已售"更新掉(revalidateTag),你不用知道后厨怎么走线、也碰不到食材库(密钥、DB)。而传统做法是你得自己跑去后厨门口(手写 /api 路由 + fetch)递条子。

Server Action 的完整数据流

① 客户端表单提交 / 调用 ② 序列化 + POST参数发往服务端 ③ 服务端执行写库 / 改状态 ④ 失效缓存revalidateTag / Path ⑤ 返回 + 重渲新 RSC payload 合并
图 7 · Server Action 写路径:调用→序列化→服务端执行→失效缓存→回传新 payload 局部更新
★ 面试高频

"Server Action 和 Route Handler(route.ts)怎么选?" Server Action 适合应用内的变更(表单提交、点赞、增删改),与 RSC/缓存失效深度集成、写法简洁、支持渐进增强;Route Handler 适合需要标准 HTTP 接口的场景(给第三方/移动端用的 REST、webhook、需要自定义响应头/流式响应)。一句话:对内变更用 Action,对外接口用 Route Handler。

▲ 安全要点

Server Action 是公开的端点——任何人都可能构造请求调用它。必须在 Action 内部自行做鉴权与输入校验(别假设只有你的表单会调它)。这是常被忽视的安全考点。

12数据流全链路:把一切串起来


这一章是全文的"会合点"。我们把前面的零件拼成一张完整的数据流图,覆盖(首屏→交互)与(变更→回流)两个方向。能把这张图讲顺,原理题基本稳了。

客户端 / 浏览器 服务端 数据源DB · API · 文件 · 第三方 缓存层Request Memo · use cache · tag RSC 渲染Server Comp await 数据→ RSC Payload SSR + Streamingpayload → HTML 分块静态壳先发 · 洞流式补 Server Action执行写操作+ revalidateTag/Path 接收 HTML + payload首屏直出(不等 JS)Router Cache 存 payload 下载 chunk + 水合客户端组件接事件/状态→ 完全可交互 用户交互 / 软导航 提交表单 / 触发变更 读路径 ▶ 软导航:仅取变化段 payload 写后失效缓存 ▲ 回传新 payload 局部更新
图 8 · 全链路数据流:红=读/写主路径,绿=客户端与服务端的局部回流

读方向(首屏)一句话串联

数据源 →(经缓存层去重/复用)→ Server Component await 取数 → 产出 RSC Payload → SSR 转 HTML 并 Streaming → 浏览器首屏直出 → 下载 chunk → 水合 → 可交互。

读方向(软导航)一句话串联

用户点 <Link> → 客户端路由器算出变化段 → 只请求该段的新 payload(命中 Router Cache 则直接用)→ 与保留布局拼接 → 局部更新。

写方向一句话串联

用户提交 → Server Action 序列化参数 POST 到服务端 → 服务端执行写操作 → revalidateTag/Path 失效相关缓存(服务端 Data Cache + 客户端 Router Cache)→ 服务端重渲生成新 payload 回传 → 客户端局部更新,UI 反映最新数据。

★ 终极串联题

"从用户在地址栏敲回车,到他点击页面上的按钮看到数据更新,整个过程发生了什么?" 这就是把图 1、图 5、图 7、图 8 串起来:① proxy 预处理 → ② 路由匹配 → ③ RSC 渲染(带缓存)产出 payload → ④ SSR+Streaming 发 HTML → ⑤ 浏览器首屏 → ⑥ 水合可交互 → ⑦ 用户点击触发 Server Action → ⑧ 服务端写库 + 失效缓存 → ⑨ 回传新 payload → ⑩ 局部更新。能把这十步讲顺,就拿下了大半。

13核心数据结构


前面流水线里"流动"的东西,落到具体形态就是这几个数据结构。把它们的形状、职责、生命周期记清楚,是回答"有哪些重要数据结构"的标准答案。

① 路由树 / 路由段(Route Tree / Segment)

文件约定被解析成一棵树,每个节点是一个 segment。它是服务端匹配请求客户端定位变化的共同依据。概念上每个节点大致携带:

路由段节点(概念形态)
type RouteSegment = { name: string // 段名,如 'dashboard'、'[id]' type: 'static' | 'dynamic' | 'catch-all' | 'group' layout?: ComponentRef // 该段的 layout.tsx page?: ComponentRef // page.tsx loading?: ComponentRef // loading.tsx(→ Suspense fallback) error?: ComponentRef // error.tsx(→ ErrorBoundary) children: RouteSegment[] // 子段 params?: string[] // 动态参数名,如 ['id'] }

② RSC Payload(Flight 数据)—— 最核心

这是整个体系的"血液":Server Component 渲染的序列化产物,既用于生成 HTML,也发给客户端用于水合与导航复用。它是一个流式的、分行的序列化结构,概念上每一"行"是一个带 id 与类型标记的条目:

条目类型承载内容
元素树Server Component 已渲染好的 React 元素树(标签、文本、属性)
模块引用遇到 Client Component 时,不内联代码,而是一个指向客户端 chunk 的引用("挂载 chunk X 里的组件 Y")
序列化 props从服务端传给客户端组件的可序列化属性
Suspense 分块挂起边界的占位,以及数据 ready 后后续补发的内容块(Streaming 的载体)
◆ 类比

RSC Payload 像一份宜家家具的图纸 + 已组装好的部件清单:大部分是已经拼好的成品(Server 渲染结果),少数地方写着"此处安装 3 号电动模块"(Client 组件的模块引用),并附上该模块的安装参数(props)。客户端拿到这份清单,去仓库(chunks)取出 3 号模块装上、通电(水合),而不需要重新生产那些已拼好的部件。

▲ 易错点

RSC Payload 不是 JSON、也不是 HTML,是 React 的专有序列化格式(社区常称 "Flight")。它能描述 React 元素、Promise、Suspense、模块引用等 JSON 表达不了的东西,并且支持流式分块。面试别说成"服务端返回 JSON 数据"。

③ Router Cache(客户端路由缓存)

浏览器内存里的一份缓存,按路由段为键存储已获取/预取的 RSC payload。生命周期是当前会话(刷新即清)。它支撑前进后退秒回与 <Link> 预取。概念形态:

Router Cache(概念形态)
// 以路由段路径为键,缓存对应的 RSC payload 与状态 type RouterCache = Map< string, // segment key,如 '/dashboard/billing' { payload: RSCPayload // 该段的渲染数据 prefetched: boolean // 是否由 Link 预取 timestamp: number // 用于判定新鲜度 } >

④ Build / Prerender 产物

构建期对静态路由生成的快照,存在 .next 里:每个可静态化路由一份 HTML + 一份 .rsc(RSC payload 快照)+ 元数据。运行时静态请求直接吐这些文件,ISR 再生时在后台更新它们。第 14 章细看。

数据结构在哪生命周期一句话职责
路由树 / Segment服务端(构建/运行)构建期确定把文件约定变成可匹配的路由结构
RSC Payload服务端产出 → 传客户端每次渲染承载组件树+引用+props,是渲染与导航的血液
Router Cache客户端内存会话内缓存 payload,支撑秒回与预取
Prerender 快照.next 磁盘 / 缓存构建→再生静态路由的 HTML+rsc,秒开走 CDN
缓存条目(tag/life)服务端缓存按 cacheLife可复用的数据缓存,按 tag 失效

14构建产物与清单文件


这一节"很实在"——讲 next build 后磁盘上到底有什么。知道这些,部署、调试、自托管时才不慌,也是面试加分细节。

.next 目录结构(概念)

.next/(生产构建产物,简化)
.next/ ├─ server/ # 服务端运行的产物 │ ├─ app/ # 各路由的服务端模块、预渲染 .html / .rsc │ ├─ chunks/ # 服务端代码分块 │ └─ *-manifest.json # 服务端用的各种清单 ├─ static/ # 发给浏览器的静态资源 │ ├─ chunks/ # 客户端 JS 分块(含 use client 组件) │ └─ css/ # 样式 ├─ cache/ # 构建/数据/图片等缓存 └─ *-manifest.json # 顶层清单(见下表)

关键清单文件(Manifests)

清单是一堆 JSON,记录"路由↔文件↔chunk"的映射与元信息,让运行时能快速找到该加载什么。常见的:

清单记录什么
routes-manifest.json所有路由的定义:静态/动态、rewrites、redirects、headers、动态路由的正则匹配规则
app-paths-manifest.jsonApp Router 各路由对应的服务端入口模块
build-manifest.json / app-build-manifest.json每个页面/路由需要加载的客户端 JS/CSS 文件列表(决定首屏要拉哪些 chunk)
prerender-manifest.json哪些路由被预渲染、其 ISR 再验证配置、回退策略
react-loadable-manifest.json动态导入(next/dynamic)的模块映射,支撑按需加载
middleware-manifest.json中间件 / proxy 的匹配规则与入口
next-font-manifest.jsonnext/font 字体优化的产物映射
★ 面试加分

被问"Next.js 怎么做代码分割的"时,可以落到清单层面:构建器按路由切 chunk,build-manifest 记录每个路由需要的 chunk 列表,运行时只下发当前路由所需的 JS;next/dynamic 的懒加载映射记在 react-loadable-manifest。这样回答比泛泛说"自动代码分割"更有深度。

注 · 部署形态

产物如何运行取决于部署目标:Vercel 会自动把静态壳放 CDN、动态部分跑在 Serverless/Edge;自托管常用 next start(Node 服务器)或 output: 'standalone'(产出自带依赖的精简包,便于打 Docker);纯静态站点可用 output: 'export'(但放弃 SSR/ISR 等动态能力)。Next 16 还引入了稳定的 Adapter API,让不同托管平台能以统一方式适配。

15proxy.ts / 中间件


在请求进入路由渲染之前,有一道可编程的"关卡"。Next 16 把它从 middleware.ts 改名并改造为 proxy.ts

它是什么、能干什么

这道关卡在请求抵达页面渲染之前运行,对所有匹配的请求生效。典型用途:鉴权拦截、重定向、URL 重写、A/B 分流、注入/改写请求与响应头、地理或语言路由。它不渲染 UI,只做"请求路由层"的决策。

proxy.ts(项目根)
import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' export function proxy(request: NextRequest) { const token = request.cookies.get('session')?.value // 未登录访问后台 → 重定向到登录页 if (!token && request.nextUrl.pathname.startsWith('/dashboard')) { return NextResponse.redirect(new URL('/login', request.url)) } return NextResponse.next() // 放行 } // 只对匹配的路径运行,避免拦截静态资源 export const config = { matcher: ['/dashboard/:path*', '/account/:path*'], }
▲ Next 16 变化

middleware.ts废弃,替代者是 proxy.ts;② 旧 middleware 运行在 Edge 运行时,新 proxy 运行在 Node.js 运行时(能力更全、更少限制);③ 旧 middleware.ts 仍可用但不建议。面试问"Next 16 破坏性变更",这是三大点之一。

▲ 安全提醒

不要把它当作唯一的鉴权防线。它适合做"边缘快速拦截 / 重定向",但敏感的授权校验仍应在数据访问层(Server Component / Server Action / Route Handler)再做一次。历史上出现过绕过 middleware 的安全公告,纵深防御是正解。

16完整请求生命周期(总装)


把所有零件按时间顺序装成一条线。这是面试"讲讲一个请求的完整过程"的标准答题脚本。

URL/导航
浏览器发起
proxy.ts
拦截/重写/鉴权
路由匹配
定位 segments
缓存检查
命中?→直出
RSC 渲染
await 数据→payload
SSR+Stream
HTML 分块发
首屏直出
壳先到
下载 chunk
按引用拉 JS
水合
接上交互
可交互
SPA 模式
图 9 · 端到端请求生命周期(首屏方向)
  1. 发起:地址栏输入或应用内导航产生请求。
  2. proxy.ts:边缘/Node 关卡先处理——可重定向(如未登录)、重写、改 header,或直接放行。
  3. 路由匹配:在路由树定位命中的 segments,组装 layout→…→page 的包裹树。
  4. 缓存检查:若该路由/片段有可用静态快照或缓存命中,直接复用,跳过重渲。
  5. RSC 渲染:运行 Server Components,await 数据(受缓存层去重/复用),产出 RSC Payload;Client 组件以模块引用形式占位。
  6. SSR + Streaming:payload 转 HTML,静态壳先发,Suspense 包裹的动态洞发 fallback、数据 ready 后补发。
  7. 浏览器首屏:边收边画,用户快速看到内容(不依赖 JS)。
  8. 下载与水合:按 payload 引用拉取客户端 chunk,水合激活交互。
  9. 进入 SPA:之后导航走客户端软导航,只取变化段 payload,命中 Router Cache 则秒回。
  10. 变更回流:用户操作触发 Server Action → 服务端写 + 失效缓存 → 回传新 payload → 局部更新。

17项目工程化要点


实际项目里除了原理,还有一组"做对就省心"的工程实践,面试也常顺带问。

推荐项目结构

典型 App Router 工程布局
src/ ├─ app/ # 路由与页面(约定式) │ ├─ layout.tsx # 根布局 │ ├─ page.tsx │ ├─ (marketing)/ # 路由组:组织而不入 URL │ ├─ dashboard/ │ │ ├─ layout.tsx │ │ └─ page.tsx │ └─ api/ # Route Handlers(对外接口) ├─ components/ # 共享组件(区分 server/client) ├─ lib/ # 数据访问、工具、db 客户端 ├─ actions/ # Server Actions("use server") └─ proxy.ts # 请求关卡

环境变量与密钥

  • NEXT_PUBLIC_ 前缀的变量会被打进客户端 bundle;密钥绝不要加该前缀
  • Server Component / Action 里可以直接读私有环境变量与 DB——因为它们不下发浏览器。这正是 RSC 的安全优势之一。

性能优化清单(高频问)

手段解决什么
"use client" 推到叶子减少客户端 JS,最大化 RSC 优势
next/image自动尺寸/格式优化、懒加载、防布局抖动(CLS)
next/font字体自托管、消除字体闪烁与外部请求
<Link> 预取 + 软导航导航近乎瞬时
<Suspense> + Streaming慢数据不阻塞首屏;配合 PPR
use cache + cacheTag精准缓存与失效,降低重复计算/查询
next/dynamic大组件按需懒加载,拆小首屏 bundle
React Compiler自动 memo 化,减少重渲
◆ 一句话总览实践哲学

Next.js 的工程心法可以浓缩成:"能在服务端做的就别推给客户端,能缓存复用的就别每次重算,能流式先给的就别让用户干等。" 这三句分别对应 RSC、Cache Components、Streaming/PPR——也正是本文的三条主线。

18面试速记 Q&A


把全文压缩成可以快速过的问答。点击问题看要点(也可直接通读)。这些覆盖了 Next.js 面试的绝大多数原理题。

QApp Router 和 Pages Router 的本质区别?
App Router 内核是 RSC,渲染默认在服务端、组件默认零 JS、渲染粒度到组件级、支持嵌套布局/Streaming/PPR;Pages Router 所有组件都是客户端组件,渲染策略页面级,靠 getServerSideProps/getStaticProps 取数。
Q什么是 React Server Components?解决什么问题?
只在服务端运行、不打包进客户端的组件。优势:零客户端 JS、可直接访问后端资源与密钥、可 async 取数。解决"客户端 bundle 过大、数据获取要往返"的问题。产物是 RSC Payload 而非 HTML。
QServer 与 Client 组件如何组合?为何 Client 不能 import Server?
Server 可渲染 Client;Client 不能直接 import Server(会把服务端代码打进浏览器),但可通过 children/props 把 Server 组件的"渲染结果"投影进 Client。因为 import 是构建期静态依赖,children 是运行期数据流。
QSSG / SSR / ISR / CSR / PPR 区别?
构建时生成=SSG;每请求生成=SSR;构建生成+后台再生=ISR;浏览器生成=CSR;静态壳(构建)+动态洞(请求流式)=PPR。App Router 把粒度下沉到组件级并用 Cache Components 统一:默认动态,use cache 才静态。
Q什么是 Hydration?常见 mismatch 原因?
客户端复用服务端 HTML、附加事件与状态而不重绘的过程。mismatch 多因两端渲染输入不一致:Date.now()/Math.random()/window 等。解决:移进 useEffectsuppressHydrationWarning、保证输入一致。
QRSC Payload 是什么?是 JSON 吗?
不是 JSON、不是 HTML,是 React 专有的流式序列化格式(Flight)。含已渲染的元素树、Client 组件的模块引用、序列化 props、Suspense 分块。用于生成 HTML + 客户端水合 + 导航复用。
QStreaming 是怎么工作的?
HTML 边算边发:静态壳先发,<Suspense>/loading.tsx 包裹的慢内容先发 fallback,数据 ready 后把真实内容作为后续 chunk 补发,浏览器原地替换占位。降低 TTFB、首屏更快。
QNext.js 有哪几层缓存?
Request Memoization(单次渲染去重)、Data Cache(跨请求数据缓存)、Full Route Cache(静态路由的 HTML+payload)、Router Cache(客户端会话内的 payload 缓存)。Next 16 用 Cache Components(use cache/cacheLife/cacheTag) 重塑了服务端持久缓存部分。
QNext 16 缓存模型最大变化?
从"隐式默认缓存"翻转为"默认动态、use cache 才缓存"。心智从"如何退出缓存"变为"给什么加缓存",更可预测;缓存粒度到组件/函数级,配合 PPR。
Q如何让数据更新后页面立即反映?
在 Server Action 里调 revalidateTag/revalidatePath(失效服务端 Data Cache 与客户端 Router Cache),或客户端 router.refresh();需要本次渲染即时一致可用 updateTag
QServer Action 是什么?和 Route Handler 怎么选?
"use server" 的函数,编译为可从客户端安全调用的服务端 RPC,适合应用内变更、与缓存失效集成、支持渐进增强。对外的标准 HTTP 接口(REST/webhook/自定义响应)用 Route Handler。Action 是公开端点,必须自做鉴权与校验。
Q客户端导航请求了什么?
不请求整页 HTML,只请求变化段的 RSC Payload(命中 Router Cache 则直接用),与保留布局拼接局部更新。外层 layout 及状态原地保留。<Link> 会自动预取视口内目标。
Qlayout 和 template 区别?
layout 导航中持久化、保留状态、不重挂载;template 每次导航重新挂载、状态清空、effect 重跑。
QTurbopack 和 SWC 分别是什么?
Turbopack 是 Rust 写的打包器(取代 webpack),Next 16 默认;SWC 是 Rust 写的编译器(取代 Babel),做 TS/JSX 转译与 Next 专属转换。
QNext 15 → 16 的破坏性变更?
① Turbopack 成默认打包器;② middleware.tsproxy.ts(Edge→Node 运行时);③ 请求 API 异步化(cookies/headers/params/searchParams 须 await,同步用法移除);④ Cache Components 新缓存模型(cacheComponents 取代 dynamicIO/ppr/unstable_cache)。
QNext.js 如何做代码分割?
构建器按路由切 chunk,build-manifest 记录每路由所需 chunk,运行时只下发当前路由 JS;next/dynamic 懒加载映射记在 react-loadable-manifest
Q怎么保护密钥不泄漏到客户端?
只有 NEXT_PUBLIC_ 前缀变量进客户端 bundle;密钥不加该前缀,仅在 Server Component/Action/Route Handler 中使用(这些代码不下发浏览器)。
QPPR 和 SSR/ISR 关系?
SSR/ISR/SSG 是页面级时机选择;PPR 把它们融进同一页:壳走构建时静态(可 CDN)、洞走请求时动态,靠 Streaming 拼接。前提是 RSC+Suspense+流式渲染。