VueSSRNuxtHydrationTeleportKeepAlive

Vue 3.6 beta.9 SSR 升級指南:Hydration、Teleport、KeepAlive 風險

彙整 Vue 3.6 beta.9 在 Hydration、Teleport、KeepAlive 的 SSR 修補重點,附 Nuxt/Vue 專案可直接套用的回歸測試矩陣與升級決策。

· 7 分鐘閱讀

這篇會帶你把 Vue 3.6 beta.9 的 changelog 轉成可執行的 SSR 回歸測試與升級決策,避免「升了版本才知道哪裡壞掉」。


前言

多數團隊看到 v3.6.0-beta.9 第一個反應通常是:「beta 先等等,等 RC 或 stable 再說。」這個策略沒有錯,但如果你在維護 SSR 專案(尤其是 Nuxt 或自架 createSSRApp),你還是需要提早做一件事:把這次修補整理成你的回歸測試矩陣。

原因很實際。Vue 3.6 beta.9 不是在推大量新 API,而是在 hydrationteleportkeep-aliveruntime-vapor 相關路徑修了很多 edge case。這類修補的特性是「你平常感受不到,一踩到就很痛」:例如 hydration anchor 對不上、Teleport target 變動後殘留節點、KeepAlive 分支切換後快取或 scope 行為異常。

截至 2026-04-02,npm dist-tags 仍是 latest: 3.5.31beta: 3.6.0-beta.9。也就是說,這一版不是所有團隊都要立刻上,但所有 SSR 團隊都該先知道「上了會影響哪裡」。

為什麼 Vue 3.6 beta.9 值得關注

先講結論:這版最值得關注的是 runtime 穩定性,不是語法糖。

minor 分支 changelog 可以看到三個明顯訊號:

  1. hydration 針對 Teleport null target、disabled teleport、nested anchor 定位連續修補。
  2. teleport 修正 target 變更時舊 anchor 清理、missing slots、deferred mount/unmount 等邊界。
  3. keep-aliveruntime-vapor 出現大量 cache key、scope cleanup、async component reactivation 相關修補。

這代表什麼?代表你就算不是在玩實驗性能力,仍可能受影響,因為 SSR 專案裡最容易爆雷的本來就不是「畫面有沒有渲染」,而是「hydration 後 DOM 與 reactive state 還有沒有對齊」。

三類高風險修補點:Hydration、Teleport、KeepAlive

下面不是逐行翻譯 changelog,而是把它轉成工程上可判斷風險的分類。

Hydration:先看 target/anchor 定位是否穩定

beta.9 這波 hydration 修補重點是 Teleport 在 null target、disabled 狀態、巢狀情境下的 anchor 處理一致性。

如果你的頁面有以下模式,建議列入高風險清單:

  1. SSR 階段 target 還不存在,client mount 後才動態插入。
  2. <Teleport :disabled="..."> 在 client 會切換啟用/停用。
  3. 巢狀 Teleport 或和 Transition / Suspense 一起使用。

這類問題在 production 常見症狀:

  1. console 出現 hydration mismatch warning。
  2. 目標容器內出現重複節點或 ghost node。
  3. 第一次切頁正常,第二次切頁才壞(因為 anchor 殘留)。

Teleport:target 變更與延遲掛載是主要地雷

beta.9 對 Teleport 的 patch 很集中在「target 改變後的清理與重掛」:

  1. target 切換時舊 anchor 清理。
  2. root replacement 後 css vars 重套。
  3. deferred teleport 在已 unmount 情境下不該繼續 mount。

工程上要特別注意兩種寫法:

  1. 依 route/state 動態改 to
  2. target 容器由外層 layout 非同步建立(例如 portal root 晚於 app mount)。

如果你的 UI 有 Drawer、Modal、Toast、Global Dropdown,幾乎都踩在這條路徑上。

KeepAlive:最常被忽略,但最容易造成「第二次才壞」

KeepAlive 在 beta.9 的修補包含 keyed branch scope、teardown、composite cache key、async component resolve 後的快取一致性。

這些詞看起來很底層,但實際影響非常「產品面」:

  1. 表單頁切換回來狀態不一致。
  2. 非同步頁面元件解完後覆蓋了已卸載分支。
  3. 記憶體緩慢上升(快取未正確 prune)。

如果你的路由層有 KeepAlive + defineAsyncComponent,或有 tab-based 多分支切換,這類回歸測試必做。

SSR 專案升級前的回歸測試矩陣(可直接落地)

下面給一套「先小後大」的測法,重點是你可以先在 app shell 做 smoke test,再決定要不要上 beta。

1. 先鎖版本並建立測試分支

# 建議在獨立分支做 beta 驗證
npm i vue@3.6.0-beta.9 @vue/server-renderer@3.6.0-beta.9

# 若是 Nuxt 專案,先確認 nuxt 與 vue peer 相容再升
npm ls vue

2. 建立 Hydration + Teleport 的最小驗證

// tests/teleport-hydration.spec.ts
import { describe, expect, it } from 'vitest'
import { createSSRApp, h, ref, Teleport } from 'vue'
import { renderToString } from '@vue/server-renderer'

describe('SSR hydration: Teleport target timing', () => {
  it('target 晚到時,不應遺留重複節點', async () => {
    const open = ref(true)

    const App = {
      setup() {
        return () =>
          h('div', [
            h('div', { id: 'app-content' }, 'page'),
            // 中文註解:故意把 Teleport 指向可能晚於 hydration 才出現的 target
            h(Teleport, { to: '#modal-root' }, open.value ? h('p', 'modal-body') : null),
          ])
      },
    }

    const html = await renderToString(createSSRApp(App))

    // 中文註解:先模擬 SSR 輸出時 target 尚未存在
    document.body.innerHTML = `<div id="app">${html}</div>`

    // 之後才插入 target
    const target = document.createElement('div')
    target.id = 'modal-root'
    document.body.appendChild(target)

    const app = createSSRApp(App)
    app.mount('#app')

    expect(document.querySelectorAll('#modal-root p').length).toBeLessThanOrEqual(1)

    app.unmount()
  })
})

3. 建立 KeepAlive + async component 切換測試

// tests/keepalive-async.spec.ts
import { defineAsyncComponent, h, KeepAlive, createApp, nextTick, ref } from 'vue'
import { describe, expect, it } from 'vitest'

describe('KeepAlive with async branches', () => {
  it('切換分支後不應殘留舊快取狀態', async () => {
    const page = ref<'A' | 'B'>('A')

    const A = defineAsyncComponent(async () => ({
      name: 'PageA',
      setup: () => () => h('div', { 'data-page': 'A' }, 'A'),
    }))

    const B = defineAsyncComponent(async () => ({
      name: 'PageB',
      setup: () => () => h('div', { 'data-page': 'B' }, 'B'),
    }))

    const App = {
      setup() {
        return () =>
          h(KeepAlive, null, [
            // 中文註解:刻意給 key,驗證 keyed branch 在反覆切換時是否穩定
            h(page.value === 'A' ? A : B, { key: page.value }),
          ])
      },
    }

    document.body.innerHTML = '<div id="app"></div>'
    const app = createApp(App)
    app.mount('#app')

    await nextTick()
    page.value = 'B'
    await nextTick()
    page.value = 'A'
    await nextTick()

    const node = document.querySelector('[data-page]')
    expect(node?.getAttribute('data-page')).toBe('A')

    app.unmount()
  })
})

4. 實際專案測試優先順序

  1. 全域 Teleport 元件(Modal/Drawer/Toast)。
  2. KeepAlive + RouterView 或 tab container。
  3. 首頁與高流量落地頁的 SSR hydration。
  4. 有 async component 的流程頁(登入、結帳、儀表板)。

Nuxt / 純 Vue 專案的升級策略

Nuxt 專案:先「驗證」再「切換預設」

對 Nuxt 專案來說,建議採兩階段:

  1. 建立 beta verification branch,只做 Vue 與關鍵相依的兼容驗證。
  2. 先把回歸測試補齊,過了再評估是否進入 staging 或灰度。

要點不是「全站立刻上 beta」,而是避免你未來升級時沒有 baseline 可比對。

純 Vue SSR 專案:可以更早吃 beta,但要有退出路徑

如果你是自管 SSR(Vite + @vue/server-renderer),通常能更快驗證。但請務必保留:

  1. 一鍵回退到 3.5.x 的 lockfile。
  2. PR 級別的 benchmark 與 memory snapshot。
  3. 針對 Teleport/KeepAlive 的 smoke test 常駐 CI。

不建議立刻升的情境

  1. 專案沒有自動化回歸測試。
  2. 近期正好在改 Router、Layout、Portal 架構。
  3. 核心頁面 heavily 依賴 KeepAlive,但目前沒有 state consistency 測試。

這三種情況建議先補測試,再談升級。

升級決策 Checklist:本週可做 / 下版本再做

本週可做

  1. 盤點所有 Teleport 使用點與 target 建立時機。
  2. 列出 KeepAlive + async component 的頁面路徑。
  3. 建立至少 3 個最小回歸測試(Hydration、Teleport、KeepAlive 各一)。
  4. 在 CI 新增一個 beta 驗證 job(不影響主線部署)。

下版本再做

  1. 若沒有足夠測試覆蓋,先維持 latest(3.5.x)。
  2. 等 RC 或 Nuxt 生態相容性訊號更清楚後,再切換生產依賴。
  3. 將 beta 驗證結果整理成團隊內升級 playbook,避免每次重頭踩雷。

常見問題 / 注意事項

這次是不是只影響 Vapor?

不是。雖然 changelog 裡有很多 runtime-vapor 條目,但 hydration / teleport / keep-alive 本身也有直接修補。你需要用「實際元件路徑」判斷風險,而不是只看功能標籤。

Nuxt 團隊一定要等 stable 嗎?

不一定。更準確的做法是「先驗證、再決策」:先在隔離分支跑回歸測試,用結果決定是否灰度,而不是用情緒判斷 beta。

Teleport 問題為什麼常在第二次操作才出現?

因為很多 bug 和 anchor 清理、目標切換、deferred mount/unmount 有關,第一次互動可能正常,第二次才暴露狀態殘留。

只靠手動測試可不可以?

可以做初步判斷,但不夠。這波修補多數是 edge case,最怕「偶發且不可重現」。至少要有可重跑的自動化測試才能做版本決策。

總結

Vue 3.6 beta.9 的價值,不在於讓你多寫幾行新語法,而是讓 SSR 專案在 Hydration、Teleport、KeepAlive 這三條最常出事故的路徑更穩。對台灣前端團隊來說,最務實的策略不是「馬上升」或「完全不看」,而是先把 changelog 轉成自己的測試矩陣。

你真正要帶回團隊的,不是一句「beta 先不要」,而是這三件事:

  1. 我們哪些頁面最容易被這波修補影響。
  2. 我們已經有沒有可重跑的回歸測試。
  3. 我們什麼條件下才會把 beta 送進 staging。

把這三件事做完,你就算今天不升,也是在替下一次升級省下真正的事故成本。

參考資料