Nuxt 3: SSR と静的生成 完全ガイド

Nuxt 3 で SSR と静的生成を使いこなしましょう。useFetch から route rules まで、Vue.js アプリケーションのパフォーマンスを最適化する方法を解説します。

Nuxt 3 と Vue.js によるサーバーサイドレンダリングと静的生成を示すイラスト

Nuxt 3 は、さまざまなユースケースに適した複数のレンダリングモードを提供することで、Vue.js アプリケーションの構築方法を大きく変えています。Server-Side Rendering(SSR)から静的生成、ハイブリッドレンダリングまで、このフレームワークはパフォーマンスと SEO を最適化するために優れた柔軟性を提供します。

前提条件

このチュートリアルは、Vue 3 と Composition API の基本的な知識を前提としています。サーバーサイドレンダリングの概念に慣れていると役立ちますが、必須ではありません。基礎的な内容はガイド全体を通じて解説していきます。

Nuxt 3 のレンダリングモードを理解する

コードに入る前に、利用できるレンダリングモードの違いを理解することが重要です。各モードは、パフォーマンス、SEO、ユーザー体験の面でそれぞれ特定のニーズに応えます。

SSR(Server-Side Rendering)はリクエストごとにサーバー上で HTML を生成します。静的生成(SSG)はビルド時にすべてのページを事前生成します。ハイブリッドモードでは、これらのアプローチをページ単位で組み合わせることができます。

nuxt.config.tstypescript
// 各種レンダリングモードの設定
export default defineNuxtConfig({
  // SSR はデフォルトで有効(SEO のために推奨)
  ssr: true,

  // 静的生成: すべてのページを事前レンダリング
  // ビルドには 'npm run generate' を使用
  // target: 'static', // Nuxt 2 の構文

  // ハイブリッドモード: ルートごとに設定可能
  routeRules: {
    // トップページ: 事前レンダリング & キャッシュ
    '/': { prerender: true },
    // ブログ: 静的生成
    '/blog/**': { prerender: true },
    // ダッシュボード: クライアントサイドのみのレンダリング
    '/dashboard/**': { ssr: false },
    // API: 事前レンダリングなし
    '/api/**': { prerender: false }
  }
})

この設定はハイブリッドモードの威力を示しています。アプリケーションの各セクションが、それぞれのニーズに最も適したレンダリングモードを使用しています。

useFetch と useAsyncData によるデータ取得

Nuxt 3 はアイソモルフィックなデータ取得のために、2 つの主要な composable を提供しています。これらの composable はサーバー側でもクライアント側でも動作し、ハイドレーションを自動的に管理します。

useFetchuseAsyncData のラッパーで、HTTP 呼び出しをシンプルにします。useAsyncData は高度なユースケースに対してより細かい制御を提供します。

vue
<script setup lang="ts">
// pages/blog/[slug].vue
// useFetch を使った記事の詳細ページ

// ルートパラメータの取得
const route = useRoute()

// useFetch: 自動的なデータ取得
// データはサーバー側で取得され、クライアントでハイドレートされます
const { data: article, pending, error } = await useFetch(
  `/api/articles/${route.params.slug}`,
  {
    // キャッシュと重複排除のための一意なキー
    key: `article-${route.params.slug}`,
    // 必要に応じてデータを変換
    transform: (response) => response.data,
    // キャッシュオプション
    getCachedData: (key) => {
      // データがキャッシュにあるかを確認
      const nuxtApp = useNuxtApp()
      return nuxtApp.payload.data[key]
    }
  }
)

// ナビゲーションを伴うエラーハンドリング
if (error.value) {
  throw createError({
    statusCode: 404,
    message: '記事が見つかりません'
  })
}
</script>

<template>
  <div>
    <div v-if="pending" class="loading">
      記事を読み込み中...
    </div>
    <article v-else-if="article">
      <h1>{{ article.title }}</h1>
      <div v-html="article.content" />
    </article>
  </div>
</template>

より細かい制御が必要な場合、useAsyncData は任意の非同期関数を実行できます。

vue
<script setup lang="ts">
// pages/products/index.vue
// useAsyncData とフィルタを使った商品一覧

const route = useRoute()

// useAsyncData: フェッチロジックを完全に制御
const { data: products, refresh } = await useAsyncData(
  'products-list',
  async () => {
    // 必要であれば複数のソースから取得
    const [productsResponse, categoriesResponse] = await Promise.all([
      $fetch('/api/products', {
        query: {
          category: route.query.category,
          sort: route.query.sort || 'date'
        }
      }),
      $fetch('/api/categories')
    ])

    // データの結合と変換
    return {
      products: productsResponse.data,
      categories: categoriesResponse.data,
      total: productsResponse.meta.total
    }
  },
  {
    // クエリパラメータが変更された際に再取得
    watch: [() => route.query]
  }
)

// 手動更新用の関数
const updateFilters = async (newCategory: string) => {
  await navigateTo({
    query: { ...route.query, category: newCategory }
  })
}
</script>

これらの composable は二重フェッチを防ぎます。サーバーで取得されたデータは HTML ペイロードにシリアライズされ、クライアントのハイドレーション時に再利用されます。

サーバーフックによる SSR のカスタマイズ

Nuxt 3 の SSR はサーバーフックを通じてカスタマイズできます。これらのフックを使うことで、レンダリングサイクルのさまざまな段階に介入し、デフォルトの挙動を変更できます。

server/plugins/render-hooks.tstypescript
// SSR レンダリングをカスタマイズするためのサーバープラグイン

export default defineNitroPlugin((nitroApp) => {
  // 各ページのレンダリング前に実行されるフック
  nitroApp.hooks.hook('render:html', (html, { event }) => {
    // スクリプトやメタデータを注入
    html.head.push(`
      <script>
        // 解析やグローバル設定
        window.__APP_CONFIG__ = {
          environment: '${process.env.NODE_ENV}',
          apiUrl: '${process.env.API_URL}'
        }
      </script>
    `)
  })

  // レンダリングキャッシュの管理用フック
  nitroApp.hooks.hook('render:response', (response, { event }) => {
    // カスタムキャッシュヘッダを追加
    const path = event.path

    if (path.startsWith('/blog/')) {
      // ブログ記事には長めのキャッシュ
      response.headers['Cache-Control'] = 'public, max-age=3600, s-maxage=86400'
    } else if (path.startsWith('/api/')) {
      // API ではキャッシュしない
      response.headers['Cache-Control'] = 'no-store'
    }
  })
})
SSR のパフォーマンス

render:response フックは HTTP キャッシュ戦略を実装するのに最適です。SSR と Cache-Control ヘッダを尊重する CDN を組み合わせることで、事前レンダリング済みのページを提供しつつ、キャッシュの無効化も維持できます。

nuxt generate による静的生成

静的生成では、ビルド時にすべてのページを事前構築します。このアプローチはブログ、ドキュメント、マーケティングサイトのように、内容が安定したサイトに最適です。

動的ルートでは、Nuxt が生成すべきすべての URL を把握する必要があります。prerender:routes フックでこれらのルートをプログラム的に定義できます。

nuxt.config.tstypescript
// 静的生成のための完全な設定

export default defineNuxtConfig({
  // 静的生成を有効化
  nitro: {
    prerender: {
      // リンクの自動クロールを有効化
      crawlLinks: true,
      // 常に含めるルート
      routes: ['/', '/about', '/contact'],
      // 一部のルートを無視
      ignore: ['/admin', '/api']
    }
  },

  hooks: {
    // 動的ルートを生成するためのフック
    async 'prerender:routes'(ctx) {
      // API または DB から記事を取得
      const articles = await fetch('https://api.example.com/articles')
        .then(res => res.json())

      // 記事のルートを追加
      for (const article of articles) {
        ctx.routes.add(`/blog/${article.slug}`)
      }

      // カテゴリを取得
      const categories = await fetch('https://api.example.com/categories')
        .then(res => res.json())

      for (const category of categories) {
        ctx.routes.add(`/category/${category.slug}`)
      }
    }
  }
})

ページ数の多いプロジェクトでは、自動クローラーでは不十分な場合があります。専用の設定ファイルを使ったより堅牢なアプローチを示します。

server/utils/generate-routes.tstypescript
// 動的ルートのリストを生成するユーティリティ

import { prisma } from './prisma'

export async function getAllStaticRoutes(): Promise<string[]> {
  const routes: string[] = []

  // ブログ記事
  const articles = await prisma.article.findMany({
    where: { published: true },
    select: { slug: true, category: { select: { slug: true } } }
  })

  for (const article of articles) {
    routes.push(`/blog/${article.category.slug}/${article.slug}`)
  }

  // 商品ページ
  const products = await prisma.product.findMany({
    where: { active: true },
    select: { slug: true }
  })

  for (const product of products) {
    routes.push(`/products/${product.slug}`)
  }

  // タグページ
  const tags = await prisma.tag.findMany({
    select: { slug: true }
  })

  for (const tag of tags) {
    routes.push(`/tags/${tag.slug}`)
  }

  return routes
}

Vue.js / Nuxt.jsの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

routeRules によるハイブリッドレンダリング

ハイブリッドレンダリングは Nuxt 3 の目玉機能です。ルートごとに異なるレンダリングルールを定義し、SSR と SSG の長所を組み合わせることができます。

nuxt.config.tstypescript
// ハイブリッドレンダリングの高度な設定

export default defineNuxtConfig({
  routeRules: {
    // マーケティングページ: 事前レンダリングし、長期間キャッシュ
    '/': { prerender: true },
    '/pricing': { prerender: true },
    '/features/**': { prerender: true },

    // ブログ: ISR (Incremental Static Regeneration)
    // 1 時間ごとに再検証
    '/blog/**': {
      isr: 3600,
      prerender: true
    },

    // ドキュメント: 再検証付きの CDN キャッシュ
    '/docs/**': {
      swr: 86400, // Stale-while-revalidate
      prerender: true
    },

    // EC: 短いキャッシュ付きの SSR
    '/products/**': {
      ssr: true,
      cache: {
        maxAge: 60,
        staleMaxAge: 300
      }
    },

    // カートとチェックアウト: クライアントサイドのみ
    '/cart': { ssr: false },
    '/checkout/**': { ssr: false },

    // ダッシュボード: SPA モード
    '/dashboard/**': {
      ssr: false,
      // 事前レンダリングを無効化
      prerender: false
    },

    // API ルート: デフォルトでキャッシュなし
    '/api/**': {
      cors: true,
      headers: {
        'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE'
      }
    }
  }
})

この設定は典型的なモダンアプリケーションのアーキテクチャを示しています。公開ページは SSG により SEO 向けに最適化され、インタラクティブなセクションはクライアントサイドレンダリングを利用しています。

データキャッシュによるパフォーマンス最適化

ページキャッシュに加えて、Nuxt 3 では取得したデータをキャッシュできます。この戦略により API 負荷を軽減し、応答時間を改善できます。

server/api/articles/[slug].get.tstypescript
// データキャッシュ付きの API エンドポイント

import { getArticleBySlug } from '~/server/utils/articles'

export default defineCachedEventHandler(
  async (event) => {
    const slug = getRouterParam(event, 'slug')

    if (!slug) {
      throw createError({
        statusCode: 400,
        message: 'slug がありません'
      })
    }

    const article = await getArticleBySlug(slug)

    if (!article) {
      throw createError({
        statusCode: 404,
        message: '記事が見つかりません'
      })
    }

    return article
  },
  {
    // slug ベースのキャッシュキー
    getKey: (event) => `article-${getRouterParam(event, 'slug')}`,
    // キャッシュ時間: 1 時間
    maxAge: 3600,
    // Stale-while-revalidate: 更新中も古いキャッシュを提供
    staleMaxAge: 7200,
    // タグベースの無効化
    tags: ['articles']
  }
)

コンテンツが変更された際にキャッシュを無効化するため、Nuxt はタグシステムを提供しています。

server/api/articles/[slug].put.tstypescript
// キャッシュ無効化を伴う記事の更新

import { updateArticle } from '~/server/utils/articles'

export default defineEventHandler(async (event) => {
  const slug = getRouterParam(event, 'slug')
  const body = await readBody(event)

  // 記事を更新
  const article = await updateArticle(slug, body)

  // この記事のキャッシュを無効化
  await useStorage('cache').removeItem(`nitro:handlers:article-${slug}`)

  // あるいはタグベースの無効化(全記事)
  // await useStorage('cache').clear('articles')

  return article
})
分散キャッシュ

複数インスタンスのプロダクション環境では、インメモリキャッシュは不十分です。インスタンス間の整合性を保つため、Nitro 設定経由で Redis などの分散システムを構成することをお勧めします。

SEO とメタデータの管理

SSR ではメタデータをサーバー側で生成することで SEO を最適化できます。Nuxt 3 はメタタグを動的に管理するための複数のアプローチを提供しています。

vue
<script setup lang="ts">
// pages/blog/[slug].vue
// SEO 最適化されたブログページ

const route = useRoute()

const { data: article } = await useFetch(`/api/articles/${route.params.slug}`)

// 記事に基づく動的な SEO 設定
useSeoMeta({
  title: article.value?.title,
  description: article.value?.excerpt,
  ogTitle: article.value?.title,
  ogDescription: article.value?.excerpt,
  ogImage: article.value?.coverImage,
  ogType: 'article',
  twitterCard: 'summary_large_image',
  twitterTitle: article.value?.title,
  twitterDescription: article.value?.excerpt,
  twitterImage: article.value?.coverImage
})

// Google 向けの構造化データ
useHead({
  script: [
    {
      type: 'application/ld+json',
      innerHTML: JSON.stringify({
        '@context': 'https://schema.org',
        '@type': 'Article',
        headline: article.value?.title,
        description: article.value?.excerpt,
        image: article.value?.coverImage,
        datePublished: article.value?.publishedAt,
        dateModified: article.value?.updatedAt,
        author: {
          '@type': 'Organization',
          name: 'SharpSkill'
        }
      })
    }
  ]
})
</script>

静的なページの場合、メタデータをコンポーネント内で直接定義できます。

vue
<script setup lang="ts">
// pages/about.vue
// SEO 付きの静的ページ

definePageMeta({
  title: '会社概要'
})

useSeoMeta({
  title: 'SharpSkill について | 技術面接対策',
  description: '技術面接対策プラットフォームの SharpSkill をご紹介します。ミッションは、開発者が技術面接で成功できるよう支援することです。',
  ogTitle: 'SharpSkill について',
  ogDescription: '技術面接対策プラットフォーム',
  ogImage: '/images/og-about.webp'
})
</script>

デプロイと本番環境での考慮事項

デプロイの選択は使用するレンダリングモードによって異なります。主要な選択肢とその設定を紹介します。

nuxt.config.tstypescript
// 各種デプロイ環境向けの設定

export default defineNuxtConfig({
  nitro: {
    // ターゲットプラットフォームに応じたプリセット
    // preset: 'vercel', // Vercel
    // preset: 'netlify', // Netlify
    // preset: 'cloudflare-pages', // Cloudflare
    // preset: 'node-server', // 一般的な Node.js

    // 本番環境の Node.js 向け設定
    preset: 'node-server',

    // レスポンスの圧縮
    compressPublicAssets: true,

    // キャッシュストレージの設定
    storage: {
      cache: {
        driver: 'redis',
        url: process.env.REDIS_URL
      }
    }
  },

  // ランタイムの環境変数
  runtimeConfig: {
    // シークレット(クライアントには公開されません)
    apiSecret: process.env.API_SECRET,
    // 公開設定
    public: {
      apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api'
    }
  }
})

静的デプロイでは、npm run generate コマンドが .output/public フォルダを生成し、任意の静的ファイルホストにそのままデプロイできます。

bash
# 静的生成
npm run generate

# .output/public の内容は次の場所にデプロイできます:
# - Vercel(自動検出)
# - Netlify(自動設定)
# - GitHub Pages
# - S3 + CloudFront
# - 任意の CDN または静的ファイルサーバー

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

まとめ

Nuxt 3 は Vue.js アプリケーションをレンダリングするための優れた柔軟性を提供します。SSR、SSG、ハイブリッドレンダリングのいずれを選ぶかは、プロジェクトごとの具体的なニーズによって決まります。

重要なポイント:

SSR: 良好な SEO が必要な動的コンテンツに最適(EC、ニュースサイト)

SSG: 安定したコンテンツに最適(ブログ、ドキュメント、マーケティングサイト)

ハイブリッド: さまざまなニーズを持つ複雑なアプリケーションに最適なアプローチ

useFetch/useAsyncData: 自動ハイドレーションとキャッシュ管理

routeRules: 各ルートの挙動をきめ細かく設定

キャッシュ: 本番環境のパフォーマンスを最適化する複数の戦略

ハイブリッドレンダリングと十分に練られたキャッシュ戦略を組み合わせることで、SEO に最適化された高速なアプリケーションを構築しつつ、Single Page Applications のインタラクティブ性も維持できます。

タグ

#nuxt 3
#vue.js
#ssr
#静的生成
#Web パフォーマンス

共有

関連記事