Nuxt4 + AIストリーミングSSR:2026年に大規模モデルアプリの初回描画を3秒から300msに最適化する実践

前端工程

Nuxt4 + AIストリーミングSSR:2026年に大規模モデルアプリの初回描画を3秒から300msに最適化する実践

AIチャットアプリの初回描画に3秒以上かかっていないか?ユーザーがメッセージを送信後、空白ページを見つめながら大規模モデルの出力完了を待つ必要があるのか?SSRレンダリングされたHTMLにはすべてのAI応答が含まれているが、ユーザーは永遠に待っている感覚になるのか?2026年、Nuxt4のストリーミングSSRはAIアプリの体験を根本的に変革する——初回描画300msで表示、ストリーミング出力がリアルタイムで表示される。


背景知識

従来のSSRがAIアプリケーションで直面する課題

次元 従来のSSR ストリーミングSSR
レンダリングモード 全データの準備完了後に一括レンダリング データの準備ができた部分から順次レンダリング
初回描画時間 AI完全応答を待機(3-30秒) 300msで初回描画スケルトン表示
ユーザー体験 長時間の空白画面 段階的にコンテンツを表示
TTFB 極めて高い(AI応答を待機) 極めて低い(HTMLヘッダーを即時返信)
ハイドレーション フルハイドレーション アイランド/プログレッシブハイドレーション
サーバーリソース 長接続の占有 ストリーミング解放

Nuxt4コア新機能

  • ストリーミングレンダリングrenderToStringがAsyncIterableをサポート、レンダリングしながら送信可能
  • Server Components.server.vueコンポーネントはサーバー側でレンダリング、JSをクライアントに送信しない
  • Edge SSR:Cloudflare Workers / Vercel Edge / Deno Deployをネイティブサポート
  • ハイブリッドレンダリング:ルートごとにSSR/SSG/SWR戦略を設定

問題分析

AIアプリケーションSSR低速の根本原因

  1. 直列待機:SSRはAI APIが完全な応答を返すまで待ってからHTMLを生成する必要がある
  2. フルハイドレーション:クライアントがすべてのコンポーネントロジックを再実行、AI呼び出しを含む
  3. ブロッキングレンダリング:1つの遅いコンポーネントがページ全体のレンダリングをブロック
  4. キャッシュ戦略なし:AI応答はキャッシュ不可、毎回のリクエストで再呼び出し

ステップバイステップ実践

ステップ1:Nuxt4プロジェクトの作成

npx nuxi@latest init ai-chat-app --template v4-compat
cd ai-chat-app
npm install
// nuxt.config.ts
export default defineNuxtConfig({
  future: {
    compatibilityVersion: 4,
  },
  experimental: {
    componentIslands: true,
    viewTransition: true,
    renderJsonPayloads: true,
  },
  routeRules: {
    '/': { ssr: true },
    '/chat/**': { ssr: true },
    '/static/**': { ssr: false },
    '/api/ai/**': { cors: true },
  },
  nitro: {
    preset: 'cloudflare-pages',
    compressPublicAssets: true,
  },
})

ステップ2:ストリーミングAI Server Componentの実装

<!-- components/ChatStream.server.vue -->
<script lang="ts" setup>
const props = defineProps<{
  messageId: string
  prompt: string
}>()

const stream = await aiStreamResponse(props.prompt)

async function* aiStreamResponse(prompt: string) {
  const response = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${useRuntimeConfig().public.aiApiKey}`,
    },
    body: JSON.stringify({
      model: 'gpt-4o',
      messages: [{ role: 'user', content: prompt }],
      stream: true,
    }),
  })

  const reader = response.body!.getReader()
  const decoder = new TextDecoder()

  while (true) {
    const { done, value } = await reader.read()
    if (done) break

    const chunk = decoder.decode(value, { stream: true })
    const lines = chunk.split('\n').filter(line => line.startsWith('data: '))

    for (const line of lines) {
      const data = line.slice(6)
      if (data === '[DONE]') return
      try {
        const parsed = JSON.parse(data)
        const content = parsed.choices[0]?.delta?.content
        if (content) yield content
      } catch {}
    }
  }
}
</script>

<template>
  <div class="chat-stream">
    <div class="message-content">
      <template v-for="(segment, i) in stream" :key="i">
        <span v-html="renderMarkdown(segment)" />
      </template>
    </div>
  </div>
</template>

ステップ3:チャットページの実装

<!-- pages/chat/[id].vue -->
<script lang="ts" setup>
const route = useRoute()
const chatId = route.params.id as string

const { data: messages, refresh } = await useFetch(`/api/chat/${chatId}/messages`)

const newMessage = ref('')
const isStreaming = ref(false)

async function sendMessage() {
  if (!newMessage.value.trim() || isStreaming.value) return

  const prompt = newMessage.value
  newMessage.value = ''
  isStreaming.value = true

  await $fetch('/api/chat/send', {
    method: 'POST',
    body: { chatId, content: prompt },
  })

  await refresh()
  isStreaming.value = false
}
</script>

<template>
  <div class="chat-container">
    <div class="messages">
      <div v-for="msg in messages" :key="msg.id" :class="['message', msg.role]">
        <div class="message-text">{{ msg.content }}</div>
      </div>
      <LazyChatStream v-if="isStreaming" :message-id="chatId" :prompt="newMessage" />
    </div>
    <div class="input-area">
      <textarea v-model="newMessage" @keydown.enter.exact.prevent="sendMessage" placeholder="メッセージを入力..." />
      <button :disabled="isStreaming" @click="sendMessage">送信</button>
    </div>
  </div>
</template>

ステップ4:APIルートのストリーミング応答実装

// server/api/chat/stream.get.ts
export default defineEventHandler(async (event) => {
  const query = getQuery(event)
  const prompt = query.prompt as string

  setResponseHeader(event, 'content-type', 'text/event-stream')
  setResponseHeader(event, 'cache-control', 'no-cache')
  setResponseHeader(event, 'connection', 'keep-alive')

  const stream = await callAIStream(prompt)

  return sendStream(event, stream)
})

async function callAIStream(prompt: string): Promise<ReadableStream> {
  const response = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.AI_API_KEY}`,
    },
    body: JSON.stringify({
      model: 'gpt-4o',
      messages: [{ role: 'user', content: prompt }],
      stream: true,
    }),
  })

  return new ReadableStream({
    async start(controller) {
      const reader = response.body!.getReader()
      const decoder = new TextDecoder()

      while (true) {
        const { done, value } = await reader.read()
        if (done) {
          controller.close()
          break
        }
        controller.enqueue(value)
      }
    },
  })
}

ステップ5:Edge SSRデプロイ設定

// nuxt.config.ts - Edge設定
export default defineNuxtConfig({
  nitro: {
    preset: 'cloudflare-pages',
    cloudflarePages: {
      routes: {
        exclude: ['/assets/*', '/_nuxt/*'],
      },
    },
  },
  experimental: {
    asyncContext: true,
  },
})
# ビルドしてCloudflare Pagesにデプロイ
npm run build
npx wrangler pages deploy .output/public

完全コード:プロダクションレベルAIチャットアプリケーション

// composables/useAIChat.ts
export function useAIChat(chatId: string) {
  const config = useRuntimeConfig()
  const messages = ref<ChatMessage[]>([])
  const isStreaming = ref(false)
  const currentStreamContent = ref('')

  async function loadMessages() {
    const { data } = await useFetch<ChatMessage[]>(`/api/chat/${chatId}/messages`)
    if (data.value) messages.value = data.value
  }

  async function sendMessage(content: string) {
    if (isStreaming.value) return

    messages.value.push({
      id: crypto.randomUUID(),
      role: 'user',
      content,
      createdAt: new Date().toISOString(),
    })

    isStreaming.value = true
    currentStreamContent.value = ''

    try {
      const response = await fetch(`/api/chat/stream?prompt=${encodeURIComponent(content)}`, {
        headers: { 'Accept': 'text/event-stream' },
      })

      const reader = response.body!.getReader()
      const decoder = new TextDecoder()

      while (true) {
        const { done, value } = await reader.read()
        if (done) break

        const chunk = decoder.decode(value, { stream: true })
        const lines = chunk.split('\n').filter(l => l.startsWith('data: '))

        for (const line of lines) {
          const data = line.slice(6)
          if (data === '[DONE]') continue
          try {
            const parsed = JSON.parse(data)
            const delta = parsed.choices[0]?.delta?.content || ''
            currentStreamContent.value += delta
          } catch {}
        }
      }

      messages.value.push({
        id: crypto.randomUUID(),
        role: 'assistant',
        content: currentStreamContent.value,
        createdAt: new Date().toISOString(),
      })
    } catch (error) {
      console.error('Stream error:', error)
    } finally {
      isStreaming.value = false
      currentStreamContent.value = ''
    }
  }

  return { messages, isStreaming, currentStreamContent, loadMessages, sendMessage }
}

interface ChatMessage {
  id: string
  role: 'user' | 'assistant' | 'system'
  content: string
  createdAt: string
}
// server/api/chat/[id]/messages.get.ts
import { kv } from '~/server/utils/kv'

export default defineEventHandler(async (event) => {
  const chatId = getRouterParam(event, 'id')!
  const cached = await kv.get(`chat:${chatId}:messages`)
  if (cached) return cached

  const messages = await db.chatMessage.findMany({
    where: { chatId },
    orderBy: { createdAt: 'asc' },
  })

  await kv.set(`chat:${chatId}:messages`, messages, { ttl: 60 })
  return messages
})
// server/middleware/cache.ts
export default defineEventHandler(async (event) => {
  if (event.path.startsWith('/api/chat/stream')) return

  const cached = await getResponseCache(event)
  if (cached) {
    return cached
  }
})

async function getResponseCache(event: H3Event) {
  const key = `cache:${event.path}`
  return await kv.get(key)
}

よくある落とし穴

落とし穴1:Server ComponentでクライアントAPIを使用

現象.server.vueコンポーネントでonClickrefなどのクライアントAPIを使用すると、ビルドエラーが発生。

解決:Server Componentはサーバー側でのみ実行でき、クライアントAPIは使用不可。インタラクティブな部分は独立したクライアントコンポーネントに抽出し、<ClientOnly>またはアイランドコンポーネントでラップする。

落とし穴2:ストリーミングレンダリング時のハイドレーションミスマッチ

現象:コンソールにHydration mismatch警告が表示され、ストリーミングコンテンツがサーバー側レンダリングと不一致。

解決:ストリーミングコンテンツを<ClientOnly>でラップするか、useAsyncDatalazy: trueオプションを使用してハイドレーションのブロックを回避。クライアントとサーバーが同じデータソースを使用することを確認。

落とし穴3:Edge RuntimeがNode.js APIをサポートしていない

現象:Cloudflare Workersにデプロイ後、process is not definedBuffer is not definedエラーが発生。

解決:Edge RuntimeにはNode.js APIが含まれていない。import { H3Event } from 'h3'をNode.jsのIncomingMessageの代わりに使用し、AI API呼び出しにはネイティブfetchを使用し、axiosなどのNode.js依存関係の使用を避ける。

落とし穴4:SSE接続がCDN層でバッファリングされる

現象:ストリーミング出力がクライアント側で一括表示され、1文字ずつ表示されない。

解決:CDNにX-Accel-Buffering: noレスポンスヘッダーを設定する。CloudflareはデフォルトでSSEパススルーをサポート。Vercelではnext.config.jsexperimental: { streaming: true }を設定する必要がある。

落とし穴5:大量の同時AIリクエストによるサーバーメモリオーバーフロー

現象:高同時接続時にNode.jsプロセスのメモリが増加し続け、OOMに達する。

解決:リクエストキューと同時接続制限を実装し、AbortControllerでタイムアウトを設定し、完了したストリーミング接続を速やかにクリーンアップする。


エラートラブルシューティング

番号 エラーメッセージ 原因 解決方法
1 Hydration mismatch サーバー側とクライアント側のレンダリング内容が不一致 ClientOnlyで動的コンテンツをラップ、データ整合性を確認
2 process is not defined Edge RuntimeがNode.js APIをサポートしていない Web標準APIで代替、nitro polyfillを追加
3 Server Component cannot use client APIs .server.vueでref/onClickを使用 インタラクティブ部分をクライアントコンポーネントに抽出
4 fetch is not a function サーバー側fetchが未設定 Node.js 18+を確認またはnitro.nodeCompatを設定
5 ReadableStream is not supported ランタイムがストリームをサポートしていない Node.js 18+にアップグレードまたはpolyfillを使用
6 CORS error on SSE クロスオリジンSSEリクエストがブロック routeRulesのcorsオプションを設定
7 KV storage not available Edge環境に永続ストレージがない Cloudflare KVまたはVercel KVを使用
8 Maximum call stack exceeded 再帰コンポーネントのレンダリングオーバーフロー コンポーネントのネストを確認、再帰深度を制限
9 429 Too Many Requests AI APIレート制限 リクエストキューとバックオフリトライを実装
10 Worker exceeded CPU time limit Edge関数のCPU時間制限超過 サーバー側の計算を削減、AI応答をストリーミング処理

高度な最適化

1. アイランドアーキテクチャでJSバンドルサイズを削減

<!-- components/AIChat.island.vue -->
<script lang="ts" setup>
defineOptions({
  island: true,
})
</script>

アイランドコンポーネントはサーバー側でのみレンダリングされ、クライアントは対応するJSをダウンロードしないため、ハイドレーションのオーバーヘッドが大幅に削減される。

2. SWRキャッシュでAI応答をキャッシュ

const { data } = await useFetch('/api/chat/messages', {
  key: `chat-${chatId}`,
  getCachedData(key, nuxtApp) {
    const cached = nuxtApp.payload.data[key]
    if (!cached) return null
    const expirationDate = new Date(cached.fetchedAt)
    expirationDate.setMinutes(expirationDate.getMinutes() + 5)
    if (expirationDate < new Date()) return null
    return cached
  },
})

3. プレレンダリングスケルトンスクリーン

<!-- components/ChatSkeleton.vue -->
<template>
  <div class="chat-skeleton animate-pulse">
    <div class="h-4 bg-gray-200 rounded w-3/4 mb-2" />
    <div class="h-4 bg-gray-200 rounded w-1/2 mb-2" />
    <div class="h-4 bg-gray-200 rounded w-5/6" />
  </div>
</template>

4. プログレッシブハイドレーション戦略

// nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    componentIslands: {
      selectiveHydration: true,
    },
  },
})

比較分析

次元 Nuxt3 SSR Nuxt4 ストリーミングSSR Next.js App Router Remix
ストリーミングレンダリング 手動実装が必要 ネイティブサポート ネイティブサポート deferサポート
Server Components なし ネイティブサポート RSC なし
Edge SSR 実験的 ネイティブサポート ネイティブサポート アダプタが必要
アイランドアーキテクチャ 実験的 安定 なし なし
AIストリーミング統合 手動 内蔵composable Vercel AI SDK 手動
ハイドレーション戦略 フル プログレッシブ/アイランド Selective フル
学習曲線
エコシステム成熟度 成熟 2026年に成熟 成熟 中程度

まとめと展望

まとめ:Nuxt4のストリーミングSSRはAIアプリケーションに質的飛躍をもたらす——3秒の空白画面から300msの初回描画可視性へ。コア最適化戦略:Server ComponentsでJSバンドルサイズを削減、ストリーミングレンダリングでAI応答をリアルタイム出力、Edge SSRでレイテンシを低減、アイランドアーキテクチャでオンデマンドハイドレーションを実現。新規AIアプリケーションはNuxt4を直接使用することを推奨、既存のNuxt3プロジェクトは段階的にアップグレードし、AIインタラクションページをストリーミングレンダリングモードに移行することを重点とする。


オンラインツールおすすめ

ブラウザローカルツールを無料で試す →

#Nuxt4#AI#SSR#流式渲染#Server Components#性能优化#大模型#Vue