渲染模式快速指南(实践派必读)
5 分钟决定一个页面该用 SSR / ISR / SSG / CSR。深入理论 + 风险背景请看 Nuxt 渲染模式选用指南。
本指南配套 framework 当前 auth 设计(device/auth 完全 client-only,详见 framework#44)。
一句话定位
ISR 主推 + SSR 守付费 + CSR 给用户态 + 混合页 hydrate
framework 不预设默认渲染策略(页面必须显式 defineRouteRules),但推荐心智按下表落地。
决策矩阵(按业务页面查表)
| 页面类型 | 渲染策略 | 建议 TTL | 关键注意 |
|---|---|---|---|
| 首页 / 分类 / 列表 / 排行榜 / 营销聚合 | ISR(主推) | 首页 600 · 分类 / 列表 / 榜单 1800 | 公开骨架走 ISR;少量用户态片段(VIP 卡 / 已购标识)用 <ClientOnly> + onMounted 拉私有 API |
| 简单详情页(介绍 / 评分 / 章节列表,无用户态) | ISR | 3600 | 纯公开数据走 ISR,性能最好 |
| 详情页(公开 + 少量用户态,最常见) | 混合策略 1:ISR 空壳 + CSR 用户态 hydrate | 3600 | 见下方决策树 + 示例代码 |
| 详情页(公开 + 大量用户态主导,个性化推荐占主体) | 混合策略 2:SSR + public/private API 分离 | 不缓存 | 见 理论指南 §十 Public/Private API 分离 |
| 阅读页 / 播放页 / 章节内容 / 视频流(付费) | SSR + token 鉴权 ⚠️ | 不缓存 | 严禁 ISR / SWR / prerender(lint 强制);防 DOM 篡改绕过付费 |
| 静态页(about / terms / 协议) | ISR | 3600 | 页头页脚的运营配置烤在 HTML 里,一小时内要能更新;不用 prerender(多域名站会被迫收敛 canonical) |
| 个人中心 / 订单 / VIP 订阅 / 钱包 | CSR (ssr: false) | 不缓存 | 全用户态,无 SEO 价值 |
| 聊天 / 打赏 / 直播 / 实时交互 | CSR (ssr: false) | 不缓存 | WebSocket / 实时数据 |
营销落地页(带 ?channel_code= 渠道码) | SSR(保留 channel)或 ISR-with-varies | 不缓存 / 600 | channel_code initCode 保留 server 端 |
| 登录 / 注册 | CSR (ssr: false) | 不缓存 | 私有 / 登录态页,禁 ISR / prerender(硬约束,与个人中心同档) |
isr只接受 60–3600 的整数秒(isr: true已移除,构建期报错,fw#414)。TTL 是「多久内必须看到更新」的上界,不是 CPU 旋钮:nitro 到期按 SWR 先回旧副本再后台重渲,长短不影响用户等待。测试环境要压低 TTL 用NUXT_ISR_TTL,不改页面声明。
详情页决策树
你的页面要 SEO 吗?
├─ 不要 → CSR (ssr: false)(用户态页 / 实时交互页)
└─ 要 → 是付费内容主体(章节 / 视频流)?
├─ 是 → SSR(带 token 鉴权,防 DOM 篡改绕过)
└─ 否 → 用户态片段占视觉多少?
├─ 无 → 纯 ISR
├─ 少(一两个 chip / row)→ ISR 空壳 + CSR hydrate(混合策略 1,详情页常见模式)
└─ 多(个性化推荐占主体)→ SSR + public/private API 分离(混合策略 2)
详情页混合策略 1 示例(最常用,背下来)
<script setup>
defineRouteRules({ isr: 3600 })
// SSR / ISR 期:拉公开主体(缓存安全)
const { data: detail } = await useAsyncData(() =>
api.manga.detail({ id: route.params.id }),
)
// 用户态片段不在 SSR 期渲染
const isFavorited = ref(false)
const purchasedChapters = ref<string[]>([])
onMounted(async () => {
// 此时 framework auth.client 已建立 token,可以打私有接口
const [fav, purchased] = await Promise.all([
$fetch('/api/user/favorite/check', { query: { id: route.params.id } }),
$fetch('/api/user/purchased-chapters', { query: { id: route.params.id } }),
])
isFavorited.value = fav.result
purchasedChapters.value = purchased
})
</script>
<template>
<article>
<SectionPublic :detail="detail" />
<ClientOnly>
<SectionUserBadges
:favorited="isFavorited"
:purchased="purchasedChapters"
/>
</ClientOnly>
<SectionChapterList :chapters="detail.chapters" />
</article>
</template>
性能与体验:
- ISR 缓存命中 → TTFB 接近静态站速度
- 用户态片段 client hydrate 后才出现(轻微闪烁,但只在小区域,不抢首屏注意力)
- 主体走 public API(不因用户而变)→ framework 在可缓存页的 SSR 期还会主动剥离身份(运行时护栏 L5)→ 缓存安全
每种策略的"能做 / 不能做"checklist
ISR / SWR / prerender 页面
✅ 能做:
useFetch公开 API(不需要鉴权的接口)- 渲染对所有用户都相同的 HTML
- SEO meta 取默认值(站点名、默认 og:image)
- 用
<ClientOnly>包用户态片段
❌ 不能做:
- 调用私有 API(带 token 鉴权)
<script setup>同步路径访问userStore.token / isLoggedIn / nickname(→ Pinia 烤进 payload 广播给所有人)useRequestHeaders(['cookie' | 'authorization'])(cookie 已被 Nitro 剥光,且使用本身就有泄漏风险)setCookie/setResponseHeader(响应头不进 ISR 缓存,等于没设)- 任何"按用户身份不同而不同"的渲染
- 在 SSR 路径读
route.query🔴 —— HTML 缓存在反代层是 path-only key(忽略全部 query),读了会让?order=hot命中?order=new的缓存、内容错配。三条修法(参数移到 path 段 /onMountedCSR 回填 / 整页ssr: false)与 path 段的边界要求见 渲染模式选用指南 §4.2⚠️
computed也算 SSR 路径:const order = computed(() => route.query.order)被 SSR 数据获取消费时就在 SSR 期求值,别以为「惰性所以不算」。eslintswift-web/no-query-in-cacheable-ssr会当场报错;onMounted/ 事件处理器里读则完全合法。
SSR (ssr: true 默认)
✅ 能做:
- 所有事情(带 token 鉴权付费内容、读 cookie 判断登录态、个性化首屏)
useFetch私有 API(cookie 自然透传 → useCookie 读 token → request util 自动注入)
❌ 不能做:
- 依赖响应被复用(每请求独立渲染,不会缓存)
- 期待 server 端自动调
loginByDevice建身份(server 期不做 auto-login,已登录用户靠 cookie 自动识别;未登录用户被 middleware 拦截到 login)
CSR (ssr: false)
✅ 能做:
- 所有用户态、私有数据交互
- WebSocket / 长轮询 / 实时数据
- 复杂表单 / 交互密集页面
❌ 不能做:
- SEO 关键内容(搜索引擎拿到的是空壳)
- 首屏要在 server 期就出现
"代码放哪儿"决策树
这段代码要在 SSR 期跑吗?
├─ 是 → 写在 <script setup> 同步路径
│ 但绝不能:touch userStore mutation(login / setInfo / getDeviceId / setAccount)
│ touch useRequestHeaders('cookie' | 'authorization')
│ touch setCookie / setResponseHeader
└─ 否 → 写在 onMounted / watch / event handler
这里可以:随便访问 userStore;调私有 API;写 cookie;任何用户态操作
反模式速查(5 个最常见错误)
❌ 反模式 1:ISR 页 <script setup> 同步读 userStore
<script setup>
defineRouteRules({ isr: 3600 })
const userStore = useUserStore() // ❌ Pinia state 烤进 payload 广播给所有人
const nickname = userStore.info?.nickname // ❌ 跨用户泄漏
</script>
✅ 正确:把 userStore 调用挪进 onMounted + 用 <ClientOnly> 包用户态 UI。
❌ 反模式 2:付费内容用 ISR
<script setup>
defineRouteRules({ isr: 3600 }) // ❌ lint 会报错
const { data: chapter } = await useFetch('/api/novel/chapter', {
query: { id, withFavorite: true } // ❌ 付费章节内容烤进缓存 = 免费泄露
})
</script>
✅ 正确:付费内容必须 defineRouteRules({ ssr: true }) 或 ssr: false。
❌ 反模式 3:ISR 页设响应 cookie
<script setup>
defineRouteRules({ isr: 3600 })
const event = useRequestEvent()
setCookie(event!, 'visited', '1', { maxAge: 3600 }) // ❌ 会被烤进缓存副本、回放给所有访客(framework 已剥离 + dev 告警)
</script>
✅ 正确:把这类副作用(追踪 cookie / 个性化 cookie)挪到 client(onMounted)。
❌ 反模式 4:ISR 页调带 token 的私有 API
<script setup>
defineRouteRules({ isr: 3600 })
const { data } = await useFetch('/api/user/orders') // ❌ 用户订单烤进 ISR 缓存
</script>
✅ 正确:
- 公开数据走 SSR/ISR
- 私有数据用
onMounted+$fetch(auth.client 已建立 token) - 详情页混合用 public/private 拆分
❌ 反模式 5:忘了写 defineRouteRules
<script setup>
// ❌ 缺 defineRouteRules → 跑在 Nuxt 默认 SSR 模式 → 性能浪费 + lint 报错
</script>
✅ 正确:每个页面都必须显式声明 defineRouteRules({ ... })。lint 会强制。
跟「理论指南」的关系
| 指南 | 何时看 |
|---|---|
| 本指南(快速指南) | 写新页面前;选不准用哪种策略时;review checklist 自查 |
| Nuxt 渲染模式选用指南(理论) | 深入理解 ISR 风险机制;遇到边界 case 想知道根因;做架构决策 |
常用 lint 规则速记
| 路径模式 | 允许 routeRules | 禁止 |
|---|---|---|
/read/**、/play/**、/chapter/**、/video/**、/watch/**、/audio/**、/episode/**、/player/**、/viewer/**(付费内容主体,默认 9 段) | ssr: true / ssr: false | isr / swr / prerender |
/mine/**、/orders/**、/settings/**、/chat/**、/live/**、/profile/**、/account/**、/wallet/**、/vip/**、/subscription(s)/**、/dashboard/**、/message(s)/**(用户态 / 实时交互,默认 14 段) | ssr: false | 其他全部 |
| 任意 page | 必须有 defineRouteRules({ ... }) 显式声明 | 缺省 |
lint 由 @swift-web/framework/eslint 提供(swift-web/no-isr-on-paid-paths / swift-web/no-isr-on-user-state-paths),在 starter eslint.config.ts 已经 extends,无需手动配置。上表为默认段名,applier 可通过规则的 segments options 覆盖。
review 自查清单(写完 page 走一遍)
- 页面声明了
defineRouteRules?策略选择符合上面的决策矩阵? - 如果是 ISR / SWR / prerender 页:HTML 输出对所有用户完全相同?
- 如果是 ISR / SWR / prerender 页:所有
useFetch/request调用都打了公开 API? - 如果是 ISR / SWR / prerender 页:
<script setup>同步路径没有 touchuserStore/useRequestHeaders('cookie')/setCookie? - 用户态片段走
onMounted+<ClientOnly>? - 付费内容主体(章节 / 视频流)路径满足 lint 规则(SSR 或 CSR,禁 ISR)?
- 至少一个多用户场景的 E2E 测试覆盖该页面?
还有疑问?
- 深入理论 + ISR 三类风险 + Public/Private API 分离方案:Nuxt 渲染模式选用指南
- framework v3 设计背景:framework#44 + #note_2847
- 业务画像 / 框架定位口号:详见 开发规范 §渲染策略