小白问:
nextjs 心智负担好重,给我整理一些需要关注的细节。
大牛:
别急,按层拆就不那么累了。下面按“必懂/易坑/进阶优化”三层给你一张速查清单,照着走就好。
一、必懂基线(先稳住基本盘)
路由与文件约定
app 目录:基于文件的路由,layout.tsx 可嵌套,page.tsx 是页面,loading/error/not-found 为内置边界。
server component 默认:组件默认在服务端渲染(RSC)。需要浏览器交互的组件用 "use client"。
路径与动态路由:[slug]、[...catchAll],生成静态/动态路径要区分清楚。
数据获取模型
Server Component 直接 await fetch/DB(推荐,少包袱)。
Route Handlers(app/api/**):写后端接口,便于前后统一。
Actions(server actions):表单/事件直调服务端函数,减少 API 层样板。
静态 vs 动态:fetch 默认缓存(静态化)。动态数据用 { cache: 'no-store' } 或 { next: { revalidate: 0 } }。
渲染与缓存大图
SSG:构建时生成,最快。适合不常变的数据。
ISR:revalidate 秒级回生,兼顾新鲜与速度。
SSR:每次请求渲染,最灵活,最贵。
边缘/节点:选择 runtime: 'edge' 适合低延迟、无 Node API 场景。
客户端与服务端边界
"use client" 组件内不能直接 await DB/秘密变量。
服务器组件不能使用浏览器 API(window、document)。
Props 下传:RSC → Client 可以,但 Client → RSC 不行(RSC 在更外层)。
二、易坑提示(减少踩坑时间)
缓存行为易混
fetch 在 RSC 内默认缓存,常见“数据不更新”的问题多因忘记设置 no-store 或 revalidate。
同一 URL + 相同 options 会被合并缓存;变 query 或 headers 才算新键。
环境变量
只有以 NEXT_PUBLIC_ 开头的变量会暴露到客户端。
服务器端可用 .env.*,Vercel 中注意环境区分与加密。
路由冲突与优先级
约定路由优先于通配,嵌套路由匹配由树结构决定。避免同级多个动态段造成歧义。
记得用 not-found.tsx 处理兜底。
CSS 与样式隔离
全局样式仅可在 app/layout.tsx 引入。
Module CSS、Tailwind、CSS-in-JS 混用时注意样式优先级与 hydration 一致性。
Hydration 不一致
客户端组件初始渲染必须与服务端输出一致;涉及时间、随机数、窗口尺寸时做条件防护或 useEffect 里处理。
图片和字体
next/image 默认优化但需正确设置域名或 loader;静态资源体积大要小心首次加载。
next/font 避免 FOUT/FOIT,记得 subset/weight 合理配置。
三、性能与可观测(进阶必修)
分层渲染策略
RSC 尽可能承载数据与重组件,客户端组件仅做交互。
使用 Suspense 分块并行加载,loading.tsx 提升感知速度。
缓存与再验证策略清单
静态页面:export const revalidate = 3600。
动态数据:fetch(url, { cache: 'no-store' })。
部分块静态、部分块动态:父级静态 + 子级用 no-store 或独立 route handler。
使用标签化 revalidateTag/unstable_cache 做细粒度失效。
边缘/函数平台选择
低延迟读多写少:Edge Runtime + KV/缓存层。
需要 Node 原生库或长时间计算:Node serverless/专用 server。
数据库直连:在 Server Components 或 Server Actions 内进行,减少 API hop。
资源分割与体积控制
避免给客户端传大对象与 DB 结果;在 RSC 里先裁剪。
动态 import 客户端组件,配合 suspense。
分析包体:next build 后用 next/script、Webpack 分析工具定位大包。
图片与流式传输
图片用 fill/priority 慎用;首屏关键图可 priority。
开启流式 SSR(默认)+ Suspense,表单用 Server Actions 提交可带来极佳 TTFB。
四、工程与可维护
目录与约定
app/(marketing)/、app/(app)/ 用分组路由管理布局与权限。
复用 UI 放在 components;仅客户端交互的组件专用 clients/ 目录,顶部统一 "use client"。
类型与安全
zod/superstruct 校验入参;边界尽量在 Server Actions 或 route handler。
TypeScript 严格模式。公共类型放 @types 或 lib/types。
错误与边界
error.tsx 捕获局部错误,global-error.tsx 捕获全局。
not-found.tsx 与 generateStaticParams 配合,防止 404/空状态尴尬。
日志与监控
接入 Vercel Analytics/Speed Insights 或 OpenTelemetry。
关键路径(登录、结算)加上 RUM 事件与后端 tracing。
五、迁移与团队协作
从 pages/ 向 app/ 迁移时,先把数据获取移到服务端(RSC),再切页面结构与 layout。
规范“何时 use client”:有状态/事件/浏览器 API/第三方 UI 需在客户端,其余尽量保持服务端。
六、快速决策树(记住这几句就够)
需要交互?组件标记 use client,否则用服务端组件。
数据是否可缓存?能缓存就设 revalidate,不能就 no-store。
是否需要 API?只是表单/按钮触发后端逻辑,用 Server Actions;需要被多端复用或外部调用,再做 API 路由。
是否追求首屏快?RSC + Suspense 分块 + 图片/字体优化 + 动态 import 客户端组件。
是否需要边缘?读多写少、低延迟优先边缘;复杂 Node 依赖留在 Node runtime。
七、常用片段(记下来)
强制动态
export const dynamic = 'force-dynamic'
// 或在 fetch 中用 { cache: 'no-store' }ISR 页面
export const revalidate = 300带标签的缓存与失效
const data = await fetch(url, { next: { tags: ['post:1'] } }).then(r => r.json())
// 变更后触发
// await revalidateTag('post:1')Server Action
'use server'
export async function save(formData: FormData) { /* ... */ }路由处理器
export async function GET(req: Request) { return Response.json({ ok: true }) }
把上述当成“操作手册 + 速查表”。先把页面尽量写成 Server Components,遇到交互再下沉到客户端组件,数据默认走缓存,有变化再指定动态,这样心智负担会小很多。