Server-Side Rendering (SSR) - 二相実行モデルとHydrationの制約
ロードマップ: Vue.js学習ロードマップ
What(何についてか)
Server-Side Rendering(SSR)は、Vueコンポーネントをブラウザではなくサーバー側でHTML文字列として描画し、その完成済みHTMLをクライアントへ返す仕組みである。クライアントは受け取った静的HTMLをすぐ表示でき、その後 Vue が hydration を行うことで、同じDOMにイベントやリアクティブ更新を結び付けて対話可能なアプリへ移行する。
SSRでは同じアプリケーションコードがサーバーとクライアントの両方で動作するため、Vueアプリは “isomorphic” または “universal” な実行モデルを持つ。
Why(なぜ必要か)
SSRの主な利点は次の通りである。
- 初回表示速度(time-to-content)の改善
- SEOの改善
- サーバーサイドとクライアントサイドを同じVueコンポーネントモデルで扱えることによる思考統一
特に初回表示速度は重要で、ブラウザがJavaScriptのダウンロードと実行を完了する前に、サーバーが返した完成済みHTMLを表示できる。これは slow network / slow device 環境で効果が大きい。
一方で、SSRには明確なコストもある。
- browser専用APIへの制約
- ビルドとデプロイの複雑化
- Node.js実行環境の必要性
- サーバーCPU負荷の増加
そのため、社内ダッシュボードのようにSEOや初回表示速度がそこまで重要でないアプリに対しては過剰設計になりやすい。
How(どう動くか)
SSRの基本フロー
flowchart LR A["Vue components"] --> B["Server renders HTML"] B --> C["Browser shows static HTML"] C --> D["Client hydration"] D --> E["Interactive Vue app"]
最小構成
SSRでは createSSRApp() と renderToString() が基本APIになる。
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
const app = createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})
renderToString(app).then((html) => {
console.log(html)
})この段階で得られるのは静的HTMLであり、クライアントでまだVueは動いていないため、画面は表示されてもボタン操作などは動作しない。
Hydration
クライアント側では同じアプリを createSSRApp() で再生成し、既に存在するSSR済みDOMへ接続する。
import { createSSRApp } from 'vue'
const app = createSSRApp({
// same app as server
})
app.mount('#app')この mount は新規DOM生成ではなく、既存DOMを引き継いでイベントやリアクティブ更新を接続する hydration として動作する。
Universal code
SSRでは同じアプリ生成ロジックを server / client の両方で再利用するため、共通の createApp() を切り出す構造が基本となる。
// app.js
import { createSSRApp } from 'vue'
export function createApp() {
return createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})
}server と client がこの共通ロジックを使うことで、同じアプリ構造を二度実行できる。
SSR vs. SSG
SSRはリクエストごとにサーバーがHTMLを生成する。一方、Static Site Generation(SSG)はビルド時に一度だけHTMLを生成し、以後は静的ファイルとして配信する。
| 観点 | SSR | SSG |
|---|---|---|
| HTML生成タイミング | リクエストごと | ビルド時 |
| ユーザーごとの差分 | 扱いやすい | 扱いにくい |
| 配信コスト | 高い | 低い |
| 運用複雑性 | 高い | 低い |
| 向く用途 | 動的ページ、初回表示重視 | docs、blog、marketing pages |
静的コンテンツ中心なら、SSRよりSSGのほうが合理的な場合が多い。
SSR特有の制約
Browser-specific code
SSRの最初の実行はNode.js上で行われるため、window、document、localStorage などの browser 専用APIはそのまま使えない。DOM依存の処理は mounted / onMounted などの client-only lifecycle に寄せる必要がある。
Lifecycle の違い
サーバー実行時には DOM を前提とする lifecycle hook は動作しない。したがって、初期データ構築とDOM副作用を分離して設計する必要がある。
Cross-request state pollution
SSRでは server process が複数リクエストをさばくため、module scope の singleton state を共有すると、あるユーザーの状態が別のユーザーに混入する危険がある。
そのため、SSRでは app、router、store を リクエストごとに新規生成 するのが原則である。
flowchart TD A["Request A"] --> S["singleton store"] B["Request B"] --> S S --> R["state contamination risk"]
Piniaのような state management ライブラリは、この問題を前提にSSR対応設計を持つ。
Hydration mismatch
サーバーが生成したHTMLと、クライアントが期待するDOM構造が一致しないと hydration mismatch が起きる。主な原因は次の通りである。
- invalid HTML nesting
- ランダム値の利用
- server / client 間の timezone 差
- server / client で異なるデータを使うこと
Vueは可能な範囲で自動復旧を試みるが、パフォーマンス低下や不安定化を招くため、開発中に原因を除去するのが基本である。
高レベルソリューション
理論上は renderToString() を使って生のSSRアプリを構築できるが、本番では次のような問題を自力で扱う必要がある。
- client build と server build の二系統管理
- SFCコンパイル
- asset link や resource hint の埋め込み
- routing、data fetching、state management の universal 対応
- SSR と SSG の切り替えや混在
そのため、実務ではNuxtのような高レベルソリューションを使うのが合理的である。SSRは原理理解と、フレームワークの制約がなぜ存在するかを理解するために学ぶ価値が高い。
Options API → Composition API 差分(補足)
| 項目 | Options API(旧) | Composition API(新) |
|---|---|---|
| アプリ生成 | createSSRApp は同じだが、内部ロジックは data / methods 中心 | setup / composable 中心で universal code を組みやすい |
| browser API 分離 | mounted() に寄せる | onMounted() に寄せる |
| shared logic | mixins で共有しがち | composable で server/client 共通ロジックを切り出しやすい |
| state 注入 | Options寄り構文で provide/inject | Composition API で provide/inject や composable を明示的に扱いやすい |
Key Concepts
| 用語 | 説明 |
|---|---|
| SSR | サーバーでVueをHTML化して返す方式 |
| hydration | 既存のSSR済みDOMへVueを接続して対話可能にする工程 |
| universal code | server/client の両方で共有されるアプリコード |
| time-to-content | ユーザーが最初に内容を見られるまでの速さ |
| SSG | ビルド時に静的HTMLを生成して配信する方式 |
| cross-request state pollution | リクエスト間で状態が混入するSSR特有の問題 |
| hydration mismatch | server と client のDOM期待が不一致になる問題 |
| createSSRApp | SSR用Vueアプリ生成API |
| renderToString | VueアプリをHTML文字列へ描画するSSR API |
実務での判断軸
- SEOや初回表示速度が極めて重要ならSSRを検討する
- 静的コンテンツ中心ならSSRよりSSGを優先する
- 社内ダッシュボードのような閉じた業務SPAではSSRは過剰になりやすい
- SSRを使うなら server/client 二相実行、singleton state 問題、hydration mismatch を前提に設計する
- 実務では生SSRを手組みするより、高レベルフレームワークを使うのが合理的である