本文 AI 產出,尚未審核

Vue3 – 效能與最佳化:SSR Hydration 效能調整


簡介

在現代 Web 開發中,Server‑Side Rendering (SSR) 已成為提升首屏渲染速度與 SEO 表現的關鍵技術。Vue 3 提供了完整的 SSR 解決方案,讓開發者可以在伺服器端預先產生 HTML,然後在瀏覽器端「hydrate」成可互動的 Vue 應用。

然而,hydration 本身也會帶來額外的 JavaScript 執行成本,特別是大型單頁應用 (SPA) 時,若不加以優化,使用者仍會感受到卡頓或長時間的阻塞。本文將從概念、實作到常見陷阱,完整說明如何在 Vue 3 中調整 SSR hydration 的效能,讓你的網站既快又穩。


核心概念

1. Hydration 是什麼?

Hydration 指的是瀏覽器收到伺服器傳回的靜態 HTML 後,把這些已渲染的節點掛載 (mount) 到 Vue 實例上,使之具備雙向資料綁定、事件監聽等互動功能。簡單來說,SSR 產生的是「可見的靜態頁面」,而 hydration 則是把「**靜態」」變成「**動態」」的過程。

重點:如果 hydration 的成本過高,使用者仍會在「看見內容」與「可以操作」之間等待較長時間,失去 SSR 的優勢。

2. 為什麼 Hydration 會消耗資源?

項目 可能的效能瓶頸
虛擬 DOM 比對 伺服器端渲染的 HTML 必須與客戶端重新產生的 VNode 結構逐一比對。
事件綁定 需要在每個節點上掛載監聽器,對大量互動元素尤為費時。
大型組件樹 整棵組件樹一次性 hydrate,會導致大量同步 JavaScript 執行。
同步資料請求 若在 setup() 中直接發起 API 請求,會阻塞 hydration 完成。

3. 基本的 Hydration 流程

flowchart TD
    A[伺服器端渲染 (SSR)] --> B[回傳 HTML + 初始 state]
    B --> C[瀏覽器載入 HTML]
    C --> D[Vue createSSRApp() 產生客戶端應用]
    D --> E[hydrate() 比對 & 事件綁定]
    E --> F[應用可互動]

4. 核心優化策略

  1. 分段 Hydration(Partial / Lazy Hydration)
    只在需要時才 hydrate 某些區塊,降低首次渲染的 JavaScript 量。

  2. 組件層級的懶載入(Async Component)
    把非首屏組件以 defineAsyncComponent 包裝,延遲載入與 hydrate。

  3. 使用 v-memo 防止不必要的比對
    讓 Vue 在相同輸入下跳過 VNode 重新渲染。

  4. Streaming + Suspense
    先送出可立即 hydrate 的內容,後續再流式傳送其餘區塊。

  5. Cache & 預取 (Prefetch) 靜態資源
    透過 HTTP cache、Service Worker 或 <link rel="preload"> 減少網路延遲。

以下將以實務範例說明每項技巧的具體寫法與效能影響。


程式碼範例

範例 1️⃣:基本的 SSR 設定與 Hydration

// server.js (Node)
import { createSSRApp } from 'vue'
import { renderToString } from '@vue/server-renderer'
import App from './src/App.vue'

export async function renderPage(url) {
  const app = createSSRApp(App)
  // 可在此加入 router、store 等
  const html = await renderToString(app)
  const state = {} // 初始 state,若有 Vuex / Pinia 需序列化
  return { html, state }
}
// client-entry.js (瀏覽器入口)
import { createSSRApp, hydrate } from 'vue'
import App from './src/App.vue'

const app = createSSRApp(App)

// 假設伺服器已把 initial state 放在 window.__INITIAL_STATE__
if (window.__INITIAL_STATE__) {
  // 把 state 注入到 store (Pinia / Vuex)
}

// **hydrate** 會自動在 #app 容器上執行比對與掛載
hydrate(app, document.getElementById('app'))

說明:此為最簡單的 SSR + Hydration 流程,適合作為後續優化的基礎。


範例 2️⃣:分段 Hydration – 只 hydrate 首屏區塊

<!-- index.html -->
<body>
  <div id="app"><!-- SSR 產生的完整 HTML --></div>

  <!-- 只 hydrate 首屏的 hero 區塊 -->
  <script type="module">
    import { createSSRApp, hydrate } from 'vue'
    import Hero from './src/components/Hero.vue'

    const app = createSSRApp({
      render: () => h('div', { id: 'hero' }, [h(Hero)])
    })
    // 只在 #hero 節點上進行 hydrate
    hydrate(app, document.getElementById('hero'))
  </script>

  <!-- 其餘非首屏區塊採用 lazy hydration -->
  <script src="/lazy-hydration.js" defer></script>
</body>
// lazy-hydration.js
import { createSSRApp, hydrate } from 'vue'
import Content from './src/components/Content.vue'

document.addEventListener('DOMContentLoaded', () => {
  const app = createSSRApp({
    render: () => h('div', { id: 'content' }, [h(Content)])
  })
  hydrate(app, document.getElementById('content'))
})

效能說明:首屏只 hydrate #hero,其餘內容在 DOMContentLoaded 後才開始,減少首次 JS 執行量,提升 First Input Delay (FID)


範例 3️⃣:Async Component + Suspense

// src/components/AsyncChart.vue
import { defineAsyncComponent } from 'vue'

export default defineAsyncComponent({
  // 只在客戶端載入圖表套件 (如 Chart.js)
  loader: () => import('./Chart.vue'),
  // 加載期間顯示 fallback
  loadingComponent: {
    template: '<div class="loading">Loading chart...</div>'
  },
  // 超時或錯誤時顯示的組件
  errorComponent: {
    template: '<div class="error">Failed to load chart.</div>'
  },
  // 延遲 200ms 再開始載入,避免短暫顯示 loading
  delay: 200,
  // 最長等待時間,超過則觸發 errorComponent
  timeout: 3000
})
<!-- 使用 Suspense 包住 AsyncChart -->
<template>
  <Suspense>
    <template #default>
      <AsyncChart />
    </template>
    <template #fallback>
      <div class="placeholder">圖表載入中…</div>
    </template>
  </Suspense>
</template>

<script setup>
import AsyncChart from '@/components/AsyncChart.vue'
</script>

效能說明AsyncChart 只在需要展示圖表時才載入,SSR 階段會輸出 fallback 的占位 HTML,減少首屏 JS 大小,同時保持 SEO。


範例 4️⃣:v-memo 防止重複比對

<!-- 假設有大量列表需要渲染 -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <!-- 若 item 本身不會變,使用 v-memo 跳過比對 -->
      <div v-memo="[item.id]">
        {{ item.name }}
      </div>
    </li>
  </ul>
</template>

<script setup>
import { ref } from 'vue'

const items = ref([
  { id: 1, name: 'Apple' },
  { id: 2, name: 'Banana' },
  // ...上千筆資料
])
</script>

說明v-memo 會在相同依賴 (item.id) 時直接重用先前的 VNode,減少 SSR Hydration 時的虛擬 DOM 比對成本,對大型列表尤為有效。


範例 5️⃣:Streaming + Suspense 結合 Node.js

// server-stream.js
import { createSSRApp, renderToPipeableStream } from '@vue/server-renderer'
import App from './src/App.vue'

export function handleRequest(req, res) {
  const app = createSSRApp(App)

  const { pipe } = renderToPipeableStream(app, {
    // 當首屏內容可供渲染時立即 flush
    onShellReady() {
      res.setHeader('Content-Type', 'text/html')
      pipe(res)
    },
    // 若渲染失敗則回傳 fallback
    onError(err) {
      console.error(err)
    }
  })
}
<!-- App.vue 中使用 Suspense -->
<template>
  <Header />
  <Suspense>
    <template #default>
      <HeavyComponent />
    </template>
    <template #fallback>
      <div class="loading">Loading…</div>
    </template>
  </Suspense>
  <Footer />
</template>

效能說明renderToPipeableStream 會在 onShellReady 時立即把已完成的 HTML 片段送給瀏覽器,先呈現可立即互動的部份,而 HeavyComponent 透過 Suspense 延遲載入,避免阻塞首屏渲染。


常見陷阱與最佳實踐

陷阱 可能的後果 解決方式
setup() 中直接呼叫同步 API Hydration 會被阻塞,導致白屏或長時間卡頓 使用 onServerPrefetch預先在伺服器端取得資料,在客戶端改用 onMounted
過度使用全局狀態 (Pinia / Vuex) 並一次性注入 初始 state 變大,傳輸與解析成本提升 分模組只注入首屏需要的 state,其餘透過 lazy fetch
將大量 CSS 直接寫在 <style> 伺服器渲染的 HTML 會攜帶巨量內嵌 CSS,影響首屏下載 使用 CSS Code‑Splitting,搭配 <link rel="preload">
忘記在 SSR 端禁用瀏覽器專屬 API(如 windowdocument SSR 失敗或產生不一致的 HTML 使用 process.client / process.server 判斷或把相關程式碼搬到 onMounted
不正確的 keyv-for Hydration 時 VNode 比對失敗,導致 DOM 重建 確保 key 為唯一且穩定的值(如 ID)

最佳實踐清單

  1. 先測量:使用 Chrome DevTools 的 PerformanceNetwork 面板,觀測 Hydration 時間與 JS bundle 大小。
  2. 分段 Hydration:將首屏與次屏分離,僅 hydrate 首屏。
  3. 組件懶載入:所有非首屏或非立即交互的組件使用 defineAsyncComponent
  4. 使用 v-memo:對於大量不變的列表或計算結果,加入 v-memo
  5. Streaming + Suspense:在 Node.js 端使用 renderToPipeableStream,搭配 <Suspense> 延遲渲染重資源組件。
  6. Cache 初始 state:將伺服器產生的 state 壓縮後放在 window.__INITIAL_STATE__,在客戶端直接注入而非再次請求。
  7. 預取關鍵腳本:在 HTML <head> 中加入 <link rel="modulepreload"><link rel="preload">,減少首次載入阻塞。

實際應用場景

場景 為何需要 Hydration 優化 可採用的技巧
電商首頁(大量商品卡片) 首屏必須快速呈現促銷資訊,且商品列表可能有上千筆 v-memo + 分段 Hydration(只 hydrate 促銷 Banner)
部落格平台(SEO 為主) 搜尋引擎需要完整 HTML,使用者則在閱讀時才需要互動 SSR 完整渲染 + Lazy Hydration(在滾動到文章底部時 hydrate 相關評論區)
資料儀表板(圖表、即時更新) 圖表套件體積龐大,首次載入不應阻塞 defineAsyncComponent + Suspense + Streaming,先顯示文字摘要,圖表稍後載入
社群平台(無限滾動) 滾動時會不斷載入新內容,若每次都完整 hydrate 會拖慢瀏覽體驗 IntersectionObserver 結合 Lazy Hydration,僅在元素進入視口時才 hydrate

總結

  • SSR Hydration 是 Vue 3 讓伺服器渲染頁面變為可互動的關鍵步驟,但同時也是效能瓶頸的常見來源。
  • 透過 分段 Hydration、Async Component、v-memo、Streaming + Suspense 等技巧,我們可以大幅降低首次 JavaScript 執行量、縮短 Time‑to‑Interactive (TTI),同時保留 SEO 與首屏可見性的優勢。
  • 在實務開發中,先測量再優化避免同步阻塞合理拆分與延遲載入 是最重要的原則。只要依照本文的最佳實踐與範例逐步調整,即可讓 Vue 3 SSR 應用在效能與使用者體驗上達到雙贏。

祝開發順利,Happy Coding! 🚀