渲染模式快速指南(實踐派必讀)
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
- 業務畫像 / 框架定位口號:詳見 開發規範 §渲染策略