SPAは初回アクセスでJavaScriptをダウンロード・解析・実行してから画面を描画するため、ネットワークや端末性能の影響を受けやすく、初期表示(FCP/LCP)が遅くなりがちです。SSR(Server-Side Rendering)を導入すると、サーバーがHTMLを先に生成して返すため、ユーザーはCSS適用後すぐにコンテンツを視認でき、その後にクライアント側でハイドレーション(イベント紐付け)を行います。本記事では、SSRの基本、実装例、Nuxtでの構成、パフォーマンス最大化のポイントを解説します。
SSRの仕組みと効果
- 仕組み:リクエストごとにVueアプリをサーバーで実行 → HTMLを生成して返却 → クライアントで同じ仮想DOMを復元(ハイドレーション)。
- 効果:初回ペイントが高速化、SEO改善、SNSシェア時のOGPレンダリングが安定。
- 注意:サーバーコスト増、実装・デバッグの複雑化、SSR非対応のブラウザAPI使用に要配慮。
ミニマルなVue 3 SSR構成(Viteベース)
学習目的の最小例です。実運用はNuxtを推奨します。
npm create vite@latest my-ssr-app -- --template vue
cd my-ssr-app
npm i
エントリ分割:クライアント・サーバー・共通を分けます。
// src/main.js(クライアント)
// クライアントはハイドレーションを実行
import { createApp } from './main.shared'
const { app } = createApp()
app.mount('#app', true) // 第2引数trueでhydrate
// src/main.server.js(サーバー)
// SSR用のエクスポート
import { createApp } from './main.shared'
import { renderToString } from 'vue/server-renderer'
export async function render(url) {
const { app } = createApp(url)
const appHtml = await renderToString(app)
return { appHtml }
}
// src/main.shared.js(共通)
import { createSSRApp, createApp as createCSRApp, h } from 'vue'
import App from './App.vue'
import { createRouter } from './router' // Vue RouterをSSR対応で構築
import { createMemoryHistory, createWebHistory } from 'vue-router'
export function createApp(url) {
const isServer = typeof window === 'undefined'
const app = (isServer ? createSSRApp : createCSRApp)(App)
const router = isServer
? createRouter(createMemoryHistory(), url)
: createRouter(createWebHistory())
app.use(router)
return { app, router }
}
// src/router.js(ルーター)
import { createRouter as _createRouter } from 'vue-router'
import Home from './pages/Home.vue'
import About from './pages/About.vue'
export function createRouter(history, initialUrl) {
const router = _createRouter({
history,
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
})
if (initialUrl) router.push(initialUrl)
return router
}
// server.js(Nodeサーバー例:Express)
import express from 'express'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import { render } from './src/main.server.js'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const template = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf-8')
const app = express()
app.use('/assets', express.static(path.join(__dirname, 'dist/assets')))
app.get('*', async (req, res) => {
try {
const { appHtml } = await render(req.url)
const html = template.replace('', appHtml)
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (e) {
res.status(500).end('Internal Server Error')
}
})
app.listen(5173, () => console.log('SSR server http://localhost:5173'))
index.html には SSR 出力を差し込む領域を用意し、クライアントスクリプトを読み込みます。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue SSR</title>
</head>
<body>
<div id="app"><!--app--></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
Nuxt 3でのSSR(実運用向け)
NuxtはSSR・ルーティング・コード分割・データ取得を統合管理します。基本はnuxt.config.ts
でSSRを有効化し、ページとデータ取得を定義するだけです。
npx nuxi init my-nuxt-app
cd my-nuxt-app
npm i
npm run dev
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true, // 既定true
routeRules: {
'/': { swr: 60 }, // 静的化 + SWRキャッシュ(Nitro)
'/about': { isr: 120 } // ISR的再生成
},
nitro: {
preset: 'node-server' // edge/adapter変更可
}
})
<!-- pages/index.vue -->
<script setup lang="ts">
const { data, pending, error } = await useFetch('/api/hello') // SSR時にサーバで取得
</script>
<template>
<div>
<p v-if="pending">Loading...</p>
<p v-else-if="error">Error</p>
<p v-else>{{ data.message }}</p>
</div>
</template>
ポイント:NuxtはSSR時にuseFetch
結果をHTMLへ埋め込み、クライアントはペイロードを復元してハイドレーションするため、初回描画が速く、APIの二重リクエストも抑制されます。
パフォーマンス最大化のための設計指針
- クリティカルCSSの適用:Fold内のスタイルはinline化、残りは遅延読み込み。
- コードスプリッティング:ルート/機能単位でチャンク分割。SSRでも有効。
- データフェッチの最小化:SSR時に必要最小限だけ取得し、残りはクライアントで遅延。
- キャッシュ戦略:HTTPキャッシュ(
Cache-Control
)、SWR/ISR、CDNキャッシュを併用。 - 画像最適化:サイズ適正化、
<img loading="lazy">
、srcset
、モダンフォーマット(WebP/AVIF)。 - ストリーミング:サーバーレンダリングのHTMLを段階的に送出(Nuxt/Nitroは対応)。
- ハイドレーション最適化:インタラクティブでない箇所はislands/部分ハイドレーションを検討。
SSR実装時の落とし穴と対策
- ブラウザAPIの使用:
window
/document
はSSRで未定義。onMounted
やprocess.client
ガードで回避。 - 状態不整合:サーバーで生成したHTMLとクライアント初期状態がズレるとハイドレーション警告。初期値をサーバーと一致させる。
- セッション/認証:CookieベースでSSR時にユーザー状態を解決。機密値をHTMLに埋め込まない。
- ビルドサイズ増:サーバーバンドルとクライアントバンドルの重複依存に注意。外部化(externals)や手動チャンクで最適化。
計測と検証
導入後、Lighthouse / WebPageTest / Chrome DevToolsで FCP・LCP・TTI を計測。webpack-bundle-analyzer や rollup-plugin-visualizerでチャンクを可視化し、クリティカルパスを短縮します。
まとめ
SSRは初期表示の高速化・SEO改善に直結する強力な手法です。Vue純正のSSR構成でも実現できますが、実運用ではデータ取得・キャッシュ・ルーティング・配信まで統合されたNuxt 3が効率的です。クリティカルCSS、コード分割、キャッシュ戦略、画像最適化を組み合わせ、計測と改善を継続することで、機能拡張とパフォーマンスを両立したプロダクション品質の体験を提供できます。