Next.js
Next.js 是建立在 React 之上的全栈 Web Framework;本页聚焦 App Router、Server/Client Components、渲染模型与应用服务器边界。
[!info] related notes
- 前置框架: React
- 核心组件模型: React Server Components, React 服务端组件与客户端组件
- 渲染模型: CSR、SSR 与 SSG, Hydration, React SSR、Hydration 与服务端组件
- 路由模型: Next.js 文件系统路由, Next.js 动态路由段, Next.js Route Groups, Next.js 客户端导航与预取
- 路由边界: Next.js Layouts, Next.js Loading 与 Streaming, Next.js Error Boundaries, Next.js Not Found 处理
- 数据获取: Next.js 数据获取边界, Next.js Server Component 数据获取, 数据所有权与服务边界, Request Waterfall 与并行数据获取, 数据获取共置
- 缓存与重新验证: Next.js Cache Components, Next.js cacheLife, Next.js Tag-based Revalidation, Next.js Cache 与 TanStack Query 的关系
- HTTP 后端边界: Next.js Route Handlers, Next.js Route Handler 与 Server Component 的边界, Next.js Route Handler、BFF 与 Webhook
- Mutation / Server Actions: Mutation(状态变更操作), Next.js Server Actions, Server Component、Server Action 与 Route Handler 的边界
- 相邻架构: Astro, Islands Architecture
- 所属入口: React MOC, 前端工程化 MOC
Next.js
这是什么
Next.js 是建立在 React 之上的全栈 Web Framework。React 主要负责组件与 UI 模型;Next.js 在其上整合路由、服务端渲染、React Server Components、服务端数据获取、HTTP 端点、Mutation、缓存、构建与部署约定。
它不是“React 的升级版”,也不是简单的“React + Express”。更稳定的理解是:
React → UI / Component model
Next.js → React application framework
App Router → route + layout + server/client boundary
Server runtime → data / secret / database / HTTP boundary
Browser runtime→ interaction / browser API / client state
App Router 的核心心智模型
现代 Next.js 应先建立两个彼此独立的维度。
维度一:组件代码在哪里执行
- Server Component:服务端执行;适合读取数据库、服务端 API、文件、Secret,并组织不依赖浏览器交互的 UI。
- Client Component:需要客户端 JavaScript runtime;适合事件、局部状态、effect、
window、document、localStorage等浏览器能力。
在 App Router 中应采用 server-first 思路:默认保持 Server Component,只有出现真实客户端能力需求时才建立 "use client" 边界。
注意:显示在浏览器里的 UI 不等于 Client Component。只根据 props 显示姓名、BMI、文章正文等内容,完全可以由 Server Component 负责。
维度二:Route 什么时候生成
- Static rendering / SSG:构建时或再验证时生成并缓存结果。
- Dynamic rendering / SSR-like request-time rendering:请求到来时根据用户、cookie、搜索参数或最新数据生成。
- CSR:主要内容依赖浏览器 JavaScript 在客户端生成或补取。
因此:
Server Component ≠ SSR
Client Component ≠ CSR
Server Component 可以参与静态生成,也可以参与请求时动态渲染;Client Component 的初始 UI 也可以被预渲染,随后在浏览器 hydration。
"use client" 真正表示什么
"use client" 不是“禁止服务端碰这个组件”,而是建立 Server → Client 模块边界:该模块及其客户端依赖需要进入客户端组件图,能够在浏览器中执行和 hydration。
常见判断:
数据库 / Secret / 服务端 I/O → Server
普通只读 JSX → 优先 Server
useState / onClick / onChange → Client
window / document / localStorage → Client
把 Client Component 设计成较小的交互边界,通常可以减少客户端 JavaScript 与 hydration 成本。
一个典型组件树
AssessmentPage Server
├── AssessmentSummary Server
├── BMIResult Server
└── WeightEditor Client
└── SaveButton Client interaction
↓
server mutation / action
↓
database
这里 SaveButton 不能简单标成“纯 Server”:点击事件发生在浏览器,真正的数据写入属于服务器。一个用户动作可以跨越 Client 与 Server 两个边界。
App Router 路由入口
App Router 不再要求维护一份独立的 route 配置表,而是让文件系统本身成为路由声明语言。
核心入口拆成四个原子知识点:
- Next.js 文件系统路由:普通目录、route segment、
page.tsx与 colocation。 - Next.js 动态路由段:
[id]、[...slug]、[[...slug]]与params。 - Next.js Route Groups:
(auth)、(app)等只组织代码、不进入 URL 的分组机制。 - Next.js 客户端导航与预取:
<Link>、客户端 Router、路由级代码分割、预取与 SPA-like navigation。
最短模型:
folder → URL segment
page.tsx → public page
[id] → dynamic segment
(group) → organize without URL
<Link> → client-side navigation / prefetch
文件系统结构可以反映产品的信息架构,但不应机械复制内部模块层级;URL nesting 也不能替代数据库关系与授权校验。
App Router 的 Route Boundary
App Router 不只有“URL → page.tsx”。每个 route segment 还可以定义共享外壳、等待状态、异常状态和资源不存在状态:
Route Segment
│
├── layout.tsx
│ ↓
│ shared / preserved shell
│
└── Route Content State
├── loading.tsx
│ ↓ pending / streaming
├── page.tsx
│ ↓ success
├── error.tsx
│ ↓ unexpected failure
└── not-found.tsx
↓ expected resource absence
对应原子笔记:
- Next.js Layouts:共享、嵌套、导航中保留的 UI 外壳,以及 Root Layout 与 Route Group 的协作。
- Next.js Loading 与 Streaming:
loading.tsx、Suspense、route-level fallback、Streaming 与细粒度 Pending Boundary。 - Next.js Error Boundaries:
error.tsx、fault isolation、reset()和父子错误边界。 - Next.js Not Found 处理:
notFound()、not-found.tsx与 404 语义。
最重要的状态区分是:
slow / pending
→ loading.tsx
success
→ page.tsx
DB timeout / unexpected code failure
→ error.tsx
query succeeds, core resource missing
→ notFound()
→ not-found.tsx
同时,外层仍然有效的 Layout 可以继续保留,因此局部 loading、error 或 not-found 不意味着整棵应用树都要重建。
App Router 数据获取入口
数据获取不能只问“Server 还是 Client”,而要同时回答:首屏是否需要、谁拥有数据、运行期是否需要持续同步。
核心入口:
- Next.js 数据获取边界:整体决策树,串起 Server Component、Client Server State、SSE 与浏览器本地状态。
- Next.js Server Component 数据获取:在服务器直接
awaitDB、内部服务或第三方 API,并把 UI-safe 数据交给组件树。 - 数据所有权与服务边界:决定“直接 DB 还是 owning service API”的不是技术可达性,而是业务 ownership。
- Request Waterfall 与并行数据获取:区分 accidental waterfall 与 dependency waterfall,用
Promise.all并行互不依赖请求。 - 数据获取共置:让数据请求靠近消费它的组件,同时保留 data/service/repository 边界。
最短模型:
首屏需要?
→ 优先 Server Component
数据是谁拥有?
→ 当前 App owner:data layer / DB
→ 独立 Service owner:service API
页面运行后持续同步?
→ TanStack Query / SWR / SSE / WebSocket
多个请求互不依赖?
→ 并行启动
特别要避免两个误区:
Client Component ≠ 必须 Client Fetch
Server Component 能访问 DB ≠ 应该绕过 Domain Service
Cache / Revalidation 入口
数据获取回答“去哪里读”,缓存进一步回答“什么时候可以复用之前的读结果、什么时候必须重新向 authority 收敛”。
当前 Cache Components 模型可以先压缩成:
要复用 server result?
→ "use cache"
时间 freshness 怎么定义?
→ cacheLife(stale / revalidate / expire)
业务数据明确变化?
→ cacheTag + on-demand revalidation
对应原子笔记:
- Next.js Cache Components:
cacheComponents、"use cache",以及 static / cached / fresh runtime 混合页面。 - Next.js cacheLife:区分
stale、revalidate、expire三种时间语义。 - Next.js Tag-based Revalidation:
cacheTag、revalidateTag、updateTag、revalidatePath与 read-your-own-writes。 - Next.js Cache 与 TanStack Query 的关系:两者都使用 identity / freshness / invalidation 思想,但属于不同缓存层。
最关键的决策:
长期稳定、允许复用
→ use cache + cacheLife
数据变化后允许短暂 stale
→ revalidateTag(tag, "max")
用户刚写完,下一次 read 必须看到 fresh
→ updateTag(tag)
高频 runtime state
→ 不用长缓存模拟实时性
→ fresh snapshot + SSE / WebSocket
还要避免三个非常容易混淆的等式:
cacheTag ≠ queryKey
updateTag ≠ optimistic update
Next.js Cache ≠ TanStack Query Cache
更准确的分层是:
Owning Service / DB
→ authoritative truth
Next.js Cache
→ server-side projection
TanStack Query
→ client-side server-state projection
因此两者不是重复实现:一个主要减少服务器读取/渲染成本,一个主要管理浏览器运行期的远程状态同步。
Route Handler / HTTP Endpoint 入口
route.ts 是 App Router 的 HTTP transport boundary,不是另一种 React Page:
page.tsx
→ URL → React UI
route.ts
→ URL → HTTP Response
对应原子笔记:
- Next.js Route Handlers:
route.ts、HTTP Method、Request/Response、动态参数、runtime validation、authentication / authorization、错误映射与 Streaming。 - Next.js Route Handler 与 Server Component 的边界:区分同应用内部 Service 调用与跨 HTTP 边界调用,避免 Server Component 绕自己的
/api/*。 - Next.js Route Handler、BFF 与 Webhook:Next.js 作为 Web-facing BFF、第三方 Webhook Endpoint、可选 SSE proxy 与 event-driven cache invalidation 的位置。
最短调用规则:
Server Component
→ 同应用业务能力
→ 直接调用 Service / Data Layer
Server Component
→ 独立 Owning Service
→ 调真正的 Service API
Browser / Mobile / External System
→ 需要 HTTP contract
→ Route Handler
Stripe / CMS / GitHub Webhook
→ Route Handler
这里最重要的不是 Next.js 发明了新后端分层,而是同一个框架同时拥有:
React Server Component
+ HTTP Route Handler
所以必须明确:
内部 server call
→ share Service
→ 不要为了复用而绕 HTTP
跨 runtime / 跨系统
→ HTTP boundary
→ Route Handler
Route Handler 本身仍然遵循普通后端工程原则:
HTTP Request
→ parse / validate
→ authenticate
→ authorize
→ Service
→ Repository / Owning Service
→ HTTP Response
尤其:
session exists
≠
can access resource
URL id
≠
authorization proof
因此 /api/assessments/123 必须继续验证当前用户对 Assessment 123 的 object-level permission,避免 IDOR / BOLA。
Server Actions / Mutation 入口
Server Action 为 React UI 提供一个直接的 server-side Mutation 边界。它简化的是 transport 样板,不会取消正常的后端工程约束。
核心原子笔记:
- Mutation(状态变更操作):Mutation 更适合理解为“状态变更操作 / 写操作 / Command”,而不是中文日常语义里的“突变”。
- Next.js Server Actions:
"use server"、React Form / Client Event、FormData、runtime validation、授权、Cache Revalidation 与 Redirect。 - Server Component、Server Action 与 Route Handler 的边界:用 Read / React Mutation / HTTP Contract 三条调用路径选择入口。
最短模型:
Server Component
→ Read / Render
Server Action
→ React UI Mutation
Route Handler
→ Explicit HTTP Contract
Server Action 典型链路:
React form / button
→ Server Action
→ validate
→ authenticate
→ authorize
→ Application Service
→ Repository / Owning Service API
→ authoritative state changes
→ revalidate cache
→ redirect / fresh UI
例如 MemoFlow Web 创建 Goal:
Create Goal
→ Server Action
→ goalService.create()
→ DB
但 MemoFlow iOS App 需要创建 Goal 时,应走稳定 HTTP Contract:
iOS
→ POST /api/goals
→ Route Handler
→ goalService.create()
如果真正数据 owner 是 BodySense Go Backend:
React UI
→ Server Action
→ Go API
→ Go Service
→ DB
不能因为 Action 在服务器运行就绕过 Go 直接写它拥有的数据库。
最重要的安全提醒:
Server Action argument
= client-controlled / untrusted input
UI hides button
≠ authorization
所以 Server Action 和 Route Handler 一样,必须在服务端重新验证输入、身份与对象级权限。
全栈不等于“不要独立后端”
Next.js 可以承担很多 Web application server / BFF 职责,例如:
- 页面与路由
- 认证入口
- CRUD
- Route Handlers
- Server Actions / Mutation
- 数据聚合
- Web 层缓存
但复杂系统仍可以保留独立服务。判断标准不是“Next.js 能不能写后端”,而是:
这段逻辑属于 Web application boundary,还是一个需要独立演进、独立扩缩容、独立权限/运行时的业务服务?
例如 AI 应用可以保持:
Next.js Web / BFF
↓
Domain Backend
↓
Agent Runtime / Worker
↓
Database
性能价值应该怎么理解
Server Components 的主要价值不是保证“总网络字节一定更少”,而是可以让很多实现代码永远不进入客户端 bundle:
- 数据库 SDK
- 服务端数据整理逻辑
- 只用于生成 UI 的大型依赖
- Secret 与内部连接逻辑
常见收益是:
client JS ↓
parse / execute JS ↓
hydration scope ↓
unnecessary client→API round trip ↓
但真实性能仍取决于 TTFB、缓存、数据访问、流式策略和客户端交互复杂度,不能把 Server Components 当作自动性能银弹。
与 Astro 的联系
Next.js 的 server-first + client boundaries 与 Islands Architecture 在目标上有相似之处:都希望只给真正需要交互的区域发送客户端 JavaScript。
但机制不同:
- Astro 更偏 HTML-first + 独立 hydration islands。
- Next.js App Router 更偏 统一 React Tree + RSC / Client Component boundary。
因此“Client Component 是一个岛”可以作为学习类比,但不能把两种机制视为同一个架构。
选择边界时的最短判断
需要浏览器能力?
├─ 否 → 优先 Server Component
└─ 是 → Client Component
需要最新/个性化请求时数据?
├─ 否 → 优先 Static rendering
└─ 是 → Dynamic request-time rendering
这两个问题要分开回答。