【Vue.js】SSR(サーバーサイドレンダリング)で初期表示を高速化する方法

【Vue.js】SSR(サーバーサイドレンダリング)で初期表示を高速化する方法 Vue.js

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で未定義。onMountedprocess.clientガードで回避。
  • 状態不整合:サーバーで生成したHTMLとクライアント初期状態がズレるとハイドレーション警告。初期値をサーバーと一致させる。
  • セッション/認証:CookieベースでSSR時にユーザー状態を解決。機密値をHTMLに埋め込まない。
  • ビルドサイズ増:サーバーバンドルとクライアントバンドルの重複依存に注意。外部化(externals)や手動チャンクで最適化。

計測と検証

導入後、Lighthouse / WebPageTest / Chrome DevToolsFCP・LCP・TTI を計測。webpack-bundle-analyzerrollup-plugin-visualizerでチャンクを可視化し、クリティカルパスを短縮します。

まとめ

SSRは初期表示の高速化・SEO改善に直結する強力な手法です。Vue純正のSSR構成でも実現できますが、実運用ではデータ取得・キャッシュ・ルーティング・配信まで統合されたNuxt 3が効率的です。クリティカルCSS、コード分割、キャッシュ戦略、画像最適化を組み合わせ、計測と改善を継続することで、機能拡張とパフォーマンスを両立したプロダクション品質の体験を提供できます。