流式渲染 Streaming SSR
传统 SSR 必须等所有数据加载完才能返回 HTML——如果一个 API 慢 2 秒,TTFB 就是 2 秒。流式 SSR(Streaming SSR)打破了这个限制:服务端可以先发送页面骨架,再逐步推送数据填充内容。用户更早看到页面框架,体验显著提升。本章深入解析 Nuxt4 流式渲染的原理、实现方式和最佳实践。
1. 传统 SSR 的瓶颈
1.1 串行等待问题
传统 SSR 的渲染流程是严格串行的:
请求到达 → 执行所有 setup()
→ 等待所有 useFetch/useAsyncData 完成
→ 渲染完整 HTML
→ 发送响应
如果页面有三个独立的数据请求:
| 请求 | 耗时 |
|---|---|
| 视频详情 | 50ms |
| 推荐列表 | 200ms |
| 评论列表 | 800ms |
虽然 Nuxt 会并行发送这三个请求(useAsyncData 自动并行),但必须等最慢的请求完成才能发送 HTML:
TTFB = max(50ms, 200ms, 800ms) + 渲染时间 ≈ 850ms
用户在 850ms 后才看到任何内容——但实际上视频详情在 50ms 就准备好了。
1.2 核心矛盾
传统 SSR 的核心矛盾是:快数据被慢数据拖累。页面大部分内容 100ms 内就能准备好,但一个慢接口拖累了整个页面的 TTFB。
2. 流式 SSR 原理
2.1 HTTP 分块传输
流式 SSR 基于 HTTP 的 Chunked Transfer Encoding——服务端不需要一次性发送完整响应,可以分多个 chunk 发送:
服务端 → [chunk 1: HTML 头部 + 页面骨架] → 浏览器开始渲染
→ [chunk 2: 视频详情数据 + HTML] → 填充视频区域
→ [chunk 3: 推荐列表数据 + HTML] → 填充推荐区域
→ [chunk 4: 评论列表数据 + HTML] → 填充评论区域
浏览器收到第一个 chunk 就开始解析和渲染——用户在 100ms 内就能看到页面骨架和视频详情,不需要等 800ms 的评论加载完。
2.2 工作流程
1. 服务端开始渲染,遇到 <Suspense> 边界
2. 先发送 <Suspense> 的 fallback(loading 占位)
3. 异步数据准备好后,发送真实内容替换 fallback
4. 浏览器通过内联 <script> 替换 DOM
关键:流式 SSR 不是简单的"分段发送 HTML",而是利用 Vue 的 <Suspense> 组件作为"分割点"——<Suspense> 的 fallback 先发送,default 等数据好了再发送。
3. Nuxt4 中的流式渲染
3.1 开启方式
Nuxt4 默认支持流式 SSR,无需额外配置。关键在于正确使用 <Suspense> 和 lazy 数据获取:
// nuxt.config.ts
export default defineNuxtConfig({
// 流式 SSR 默认开启,无需额外配置
// 但需要确保部署平台支持流式响应(Node.js 服务器、Cloudflare Workers 等)
})3.2 NuxtPage 内置 Suspense
<NuxtPage> 组件内置了 <Suspense> 包裹。这意味着页面组件中的 await useFetch() 默认就会触发 Suspense 行为。
3.3 手动 Suspense 边界
对于页面内的独立区域,可以手动添加 <Suspense> 边界:
<template>
<div>
<!-- 视频详情:快速加载,立即渲染 -->
<VideoHeader :video="video" />
<!-- 推荐列表:独立 Suspense 边界 -->
<Suspense>
<LazyVideoRecommendations :video-id="video.id" />
<template #fallback>
<div class="grid grid-cols-4 gap-4">
<div v-for="i in 4" :key="i" class="aspect-video bg-gray-200 animate-pulse rounded" />
</div>
</template>
</Suspense>
<!-- 评论区:独立 Suspense 边界 -->
<Suspense>
<LazyVideoComments :video-id="video.id" />
<template #fallback>
<div class="space-y-4">
<div v-for="i in 3" :key="i" class="h-20 bg-gray-200 animate-pulse rounded" />
</div>
</template>
</Suspense>
</div>
</template>每个 <Suspense> 都是一个独立的流式分割点——数据好了就发送,互不阻塞。
4. lazy 模式数据获取
4.1 useFetch 的 lazy 选项
// 默认模式:阻塞渲染,等数据好了再发送 HTML
const { data: video } = await useFetch(`/api/videos/${id}`)
// lazy 模式:不阻塞渲染,先发送 loading 状态
const { data: comments, pending } = await useFetch(`/api/videos/${id}/comments`, {
lazy: true,
})默认模式 vs lazy 模式的区别:
| 特性 | 默认模式 | lazy 模式 |
|---|---|---|
| SSR 阻塞 | ✅ 等数据好了再渲染 | ❌ 先渲染 pending 状态 |
| 首次 HTML 中有数据 | ✅ | ❌(需要客户端 Hydration 后填充) |
| TTFB 影响 | 增加 TTFB | 不增加 TTFB |
| 适用场景 | 关键数据(SEO 需要) | 非关键数据(评论、推荐) |
4.2 混合策略
一个页面中通常混合使用两种模式:
// 视频详情:关键数据,阻塞等待(SEO 需要)
const { data: video } = await useFetch(`/api/videos/${id}`)
// 推荐列表:非关键,lazy 加载
const { data: recommendations, pending: recPending } = await useFetch(
`/api/videos/${id}/recommendations`,
{ lazy: true }
)
// 评论列表:非关键,lazy 加载
const { data: comments, pending: commentsPending } = await useFetch(
`/api/videos/${id}/comments`,
{ lazy: true }
)这样视频详情立即渲染(SEO 爬虫能看到),推荐和评论异步加载不拖累 TTFB。
4.3 useLazyFetch 语法糖
useLazyFetch 等价于 useFetch({ lazy: true }):
// 这两行等价
const { data } = await useFetch('/api/data', { lazy: true })
const { data } = await useLazyFetch('/api/data')5. 流式渲染的边界条件
5.1 SEO 考量
搜索引擎爬虫对流式 SSR 的处理:
- Googlebot:支持流式 HTML,能等待完整内容
- 其他爬虫:可能只处理第一个 chunk
经验法则:SEO 关键内容(标题、描述、主体内容)放在第一个 chunk 中(不用 lazy),非关键内容(评论、推荐)可以 lazy 加载。
5.2 错误处理
流式响应一旦开始发送,就无法修改 HTTP 状态码。如果第一个 chunk 已经发送了 200,后续数据加载失败只能在 HTML 中处理(不能改成 500):
<Suspense>
<LazyVideoComments :video-id="video.id" />
<template #fallback>
<div class="animate-pulse">加载评论中...</div>
</template>
</Suspense>
<!-- 如果评论加载失败,组件内部处理 -->
<!-- VideoComments.vue -->
<template>
<div v-if="error" class="text-red-500">
评论加载失败,<button @click="refresh">重试</button>
</div>
<div v-else>{{ data }}</div>
</template>5.3 Head 管理
流式渲染的 <head> 标签在第一个 chunk 就发送了。后续 chunk 中通过 useHead 设置的 meta 无法插入到已发送的 <head> 中。Nuxt 通过 Payload 机制在客户端 Hydration 时补全。
5.4 部署平台兼容性
| 平台 | 流式支持 |
|---|---|
| Node.js 服务器 | ✅ |
| Cloudflare Workers | ✅ |
| Vercel Edge | ✅ |
| AWS Lambda | ⚠️ 需要 Response Streaming 配置 |
| 静态托管(Netlify Static) | ❌ 不支持 SSR |
6. AI 视频推荐列表流式渲染实战
6.1 场景分析
视频详情页的数据来源:
| 数据 | API 耗时 | 重要性 | 策略 |
|---|---|---|---|
| 视频元信息 | 20ms | 核心(SEO) | 阻塞等待 |
| 视频播放地址 | 50ms | 核心 | 阻塞等待 |
| AI 推荐列表 | 500ms | 辅助 | lazy + Suspense |
| 评论列表 | 300ms | 辅助 | lazy + Suspense |
| 作者信息 | 30ms | 辅助 | 可以阻塞(很快) |
6.2 实现
<!-- pages/videos/[id].vue -->
<template>
<div class="max-w-[1400px] mx-auto">
<!-- 第一个 chunk:立即发送 -->
<VideoPlayer :video="video" />
<VideoMeta :video="video" :author="author" />
<div class="grid grid-cols-3 gap-8 mt-8">
<div class="col-span-2">
<!-- 第三个 chunk:评论准备好后发送 -->
<Suspense>
<LazyVideoComments :video-id="video.id" />
<template #fallback>
<CommentsSkeleton />
</template>
</Suspense>
</div>
<div>
<!-- 第二个 chunk:推荐准备好后发送 -->
<Suspense>
<LazyVideoRecommendations :video-id="video.id" />
<template #fallback>
<RecommendationsSkeleton />
</template>
</Suspense>
</div>
</div>
</div>
</template>
<script setup>
const route = useRoute()
const id = route.params.id
// 关键数据:阻塞等待
const [{ data: video }, { data: author }] = await Promise.all([
useFetch(`/api/videos/${id}`),
useFetch(`/api/videos/${id}/author`),
])
if (!video.value) throw createError({ statusCode: 404, message: '视频不存在' })
</script>6.3 时序对比
传统 SSR:
0ms 850ms
|——————————————————————————————————————————|→ 发送完整 HTML
等待所有数据... TTFB: 850ms
流式 SSR:
0ms 70ms 370ms 570ms
|——————|———————————————————|——————————————|
骨架 视频+作者 评论 推荐
TTFB: 70ms(用户立即看到内容)
TTFB 从 850ms 降到 70ms——用户体验提升 12 倍。
7. 流式渲染的权衡
7.1 适合流式渲染
- 页面有多个独立数据源,耗时差异大
- 非关键内容可以延迟显示
- TTFB 是性能瓶颈
7.2 不适合流式渲染
- 所有数据都很快(< 100ms),流式带来的收益不明显
- 页面内容相互依赖(B 依赖 A 的数据)
- 目标平台不支持流式响应(Lambda 无 Streaming)
7.3 与 ISR 的互补
流式 SSR 和 ISR 可以结合使用:
- ISR 缓存命中:直接返回缓存的完整 HTML(最快)
- ISR 缓存未命中:触发 SSR,此时流式渲染减少首次 TTFB
// 两者结合
export default defineNuxtConfig({
routeRules: {
'/videos/**': { isr: 3600 }, // 缓存 1 小时
// 缓存未命中时,流式 SSR 自动生效
},
})本章小结
- 核心问题:传统 SSR 中快数据被慢数据拖累,TTFB = max(所有请求)
- 流式原理:HTTP Chunked Transfer + Vue Suspense 分割点,数据好了就发送
- 实现方式:
<Suspense>包裹 +Lazy组件 +lazy: true数据获取 - 混合策略:SEO 关键数据阻塞等待,辅助内容 lazy 加载
- 注意事项:状态码在第一个 chunk 确定、Head 在第一个 chunk 发送、部署平台兼容性
- 与 ISR 互补:ISR 解决重复请求,流式 SSR 解决首次请求的 TTFB