模块三 · 工程与体验
SSR 与 Nuxt:首屏、水合,以及"该不该上"
SSR 不是性能银弹,它用服务端成本换首屏与 SEO。这一节讲清四种渲染模式的取舍、Nuxt 的数据约定、水合不一致的排查,以及什么时候不该上。
读完这一节,你能做到
- 说清 SSR 解决什么、代价是什么,而不是"首屏更快"。
- 在 CSR / SSR / SSG / ISR 之间做出有依据的选择。
- 用 Nuxt 的数据约定避免"服务端请求一次、客户端再来一次"。
- 定位 hydration mismatch,而不是靠
suppressHydrationWarning盖住。
一、SSR 到底解决什么
三个真实收益
- 首屏内容更快可见:HTML 到达时已带内容,不需要等 JS 下载、执行、再请求接口。
- SEO 与分享卡:爬虫与社交平台拿到的是带内容的 HTML,而不是空壳。
- 弱网/低端设备体验:渲染在服务端做完,客户端只负责水合。
三个代价
| 代价 | 具体表现 |
|---|---|
| 服务端成本 | 每个请求都要跑一次渲染,QPS 高时比静态贵得多 |
| 复杂度 | 生命周期、浏览器 API、第三方库都要考虑"服务端能不能跑" |
| 缓存难做 | 个性化内容不能直接缓存,公共内容才适合 |
一句话:SSR 是用"服务端算力"换"首屏与可索引性"。如果你的用户已经在用 App、或者页面在登录后才可见,这笔交易可能不划算。
二、四种渲染模式怎么选
| 模式 | 内容生成时机 | 适合 | 不适合 |
|---|---|---|---|
| CSR | 浏览器里 | 后台系统、登录后页面 | 营销页、需要 SEO 的公开页 |
| SSR | 每次请求 | 个性化 + 需要 SEO(商品详情、资讯) | 高 QPS 且内容不变的页面 |
| SSG | 构建时 | 文档、博客、营销落地页 | 频繁变更的数据 |
| ISR | 构建后按周期再生 | 内容变化不频繁的列表与详情 | 强实时数据 |
判断顺序
- 需要 SEO 吗?不需要 → CSR 就够,别引入复杂度。
- 内容对所有人一样吗?一样 → SSG 或 ISR。
- 内容因人而异且需要 SEO → SSR。
一个常见误判
后台管理系统上了 SSR:既没 SEO 收益,又多了一层服务端渲染成本。这类系统 CSR 更合适。
三、Nuxt 的关键约定
目录即结构
pages/下的文件路径就是路由,动态段用方括号。server/api/下的文件就是后端接口,前端可以直接调。composables/自动导入,不用手动 import。middleware/放路由中间件,鉴权常放这里。
数据获取的两个入口
useAsyncData | useFetch | |
|---|---|---|
| 本质 | 通用异步数据封装 | 基于 $fetch 的语法糖 |
| 适合 | 任意异步逻辑(不只是 HTTP) | 直接调接口 |
| 关键能力 | 服务端取到的数据会随页面传给客户端 | 同左 |
为什么这件事重要
如果服务端取过数据、客户端又取一次,你就同时付了两次请求的代价,还可能出现"首屏是旧数据、水合后跳变"。用这两个 API 的目的之一就是把服务端结果传给客户端,避免二次请求。
$fetch 在两端的行为差异
同一段代码在服务端执行时是直接发请求,在客户端执行时是浏览器请求。所以不要在组件顶层直接 await $fetch,那会导致服务端与客户端各跑一次。
四、水合不一致怎么查
现象:控制台报 hydration mismatch,页面内容闪一下或事件失效。
五类常见原因
- 时间/随机数:渲染里用了当前时间、随机数,两端结果必然不同。
- 浏览器 API 判断:
window、localStorage在服务端不存在,条件渲染分支不一致。 - HTML 结构不合法:比如块级元素嵌在
<p>里,浏览器会自动纠正,导致结构对不上。 - 第三方脚本改 DOM:广告、翻译插件在水合前改了节点。
- 数据不同步:服务端用了一份数据,客户端首帧用了另一份。
排查方法
- 用"服务端渲染出的 HTML"和"客户端首帧 DOM"对比,找出第一个不同的节点。
- 二分定位:先把可疑组件改成只在客户端渲染(
<ClientOnly>),看问题是否消失。
什么时候 suppressHydrationWarning 才算正解
只在"这个差异是预期的"时用,比如显示相对时间(“3 分钟前”)这类服务端与客户端允许不同的文本。用它盖住结构性差异,等于把 bug 藏起来。
五、什么时候不该上 SSR
- 页面在登录后才可见,且不需要 SEO。
- 内部系统、后台、工具类页面。
- 团队没有 Node 服务运维能力,只有静态托管。
- 首屏瓶颈其实是接口慢,而不是渲染方式——换 SSR 不会让接口变快。
替代方案:静态预渲染 + 客户端数据请求,往往能用 20% 的复杂度拿到 80% 的收益。
六、常见坑
- 把 SSR 当性能银弹。接口慢的页面,SSR 只会让服务端也一起等。
- 组件顶层直接
await $fetch。两端各请求一次。 - 在渲染里用时间/随机数。必然水合不一致。
- 用
suppressHydrationWarning掩盖结构差异。问题被藏起来,后续更难查。 - 个性化内容不做区分就上 CDN 缓存。会把 A 的内容发给 B。
- 第三方库不判断运行环境。服务端执行时报
window is not defined。
七、动手任务:自写 → AI 写 → 对比 → 取舍
四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。
① 自己写
- 给一个"商品详情页"做渲染模式选型:写出四种模式的成本与收益,给出结论和理由。
- 写一段带数据获取的页面代码,要求服务端取一次、客户端不重复请求。
② 让 AI 写同一题
把第 2 题交给 AI,补一句 用 Nuxt 3 的约定,说明数据是怎么从服务端传到客户端的,不要用组件顶层 await。
③ 对比要盯的点
- 它有没有在组件顶层写
await $fetch? - 有没有解释"数据随页面传输"这一层,还是只说"用 useFetch 就行"?
- 选型部分是不是只说了"SSR 首屏快"?
④ 一句话取舍
它给的方案,在你的 QPS 下服务端扛得住吗?算一下再说。
八、自测
- SSR 的三个收益与三个代价分别是什么?
- CSR / SSR / SSG / ISR 各适合什么场景?
useAsyncData与useFetch的区别是什么?为什么重要?- 为什么不能在组件顶层直接
await $fetch? - 水合不一致的五类常见原因是什么?
- 什么情况下
suppressHydrationWarning才算正解?
参考答案要点:首屏、SEO、弱网,服务端成本、复杂度、缓存;按 SEO、内容是否一致、是否个性化三步判断;前者通用异步、后者基于 $fetch,服务端结果可随页面传给客户端;两端各请求一次;时间随机、浏览器 API、结构不合法、第三方改 DOM、数据不同步;只在预期差异(相对时间)时用。
配套题目
★ 越多越该先背;点题目可看完整答案与深入版。
提示:读完可以顺手做几道对应题目,检验是否真的理解