加载态与流式渲染
本教程共 42 篇 · 第 14 篇 · 更新于 2026-07-30 · 约 8 分钟阅读
14. 加载态与流式渲染
本节目标:学会用 loading.tsx 和 Suspense 创建加载态,理解流式 SSR 的工作原理,掌握提升感知性能的技巧。
什么是流式渲染
传统 SSR 要等所有数据加载完才发送 HTML。一个慢查询就能卡住整个页面。
流式渲染改变了这一点——服务器先生成能生成的部分,立即发送给客户端。数据就绪的部分陆续填入。用户看到页面在”渐进式”出现。
传统 SSR:[等待...等待...等待...] -> 一次性显示全部
流式渲染:[立即显示骨架] -> [填入头部] -> [填入内容] -> [填入推荐]
loading.tsx
最简单的加载态:在路由文件夹里放一个 loading.tsx:
app/
└── dashboard/
├── loading.tsx # 加载态
├── page.tsx # 实际页面
└── layout.tsx # 布局
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse">
<div className="h-8 w-48 bg-gray-200 rounded mb-4" />
<div className="h-4 w-full bg-gray-200 rounded mb-2" />
<div className="h-4 w-full bg-gray-200 rounded mb-2" />
<div className="h-4 w-2/3 bg-gray-200 rounded" />
</div>
)
}
Next.js 会自动把 page.tsx 包裹在 <Suspense> 里,用 loading.tsx 作为 fallback。
工作原理
layout.tsx(立即渲染)
└── loading.tsx(立即显示)
└── page.tsx(数据加载完后替换 loading)
导航时:
- 布局立即显示
- 骨架屏立即显示
- 页面数据加载完后,替换骨架屏
预取
loading.tsx 的 fallback 会被预取。用户点击链接时,骨架屏立即出现——即使数据还在加载。这让导航感觉是即时的。
Suspense 组件
loading.tsx 适合整页加载态。更细粒度的控制用 <Suspense>:
import { Suspense } from 'react'
export default function Page() {
return (
<div>
<header>
<h1>仪表盘</h1>
</header>
<main>
<Suspense fallback={<p>加载收入...</p>}>
<Revenue />
</Suspense>
<Suspense fallback={<p>加载订单...</p>}>
<RecentOrders />
</Suspense>
</main>
</div>
)
}
每个 <Suspense> 独立加载,互不阻塞。
并行加载
多个 <Suspense> 同时加载,谁先完成谁先显示:
export default function Dashboard() {
return (
<div>
<h1>仪表盘</h1>
<div className="grid grid-cols-2 gap-4">
<Suspense fallback={<Skeleton />}>
<Revenue /> {/* 200ms 完成 */}
</Suspense>
<Suspense fallback={<Skeleton />}>
<RecentOrders /> {/* 1s 完成 */}
</Suspense>
</div>
<Suspense fallback={<Skeleton />}>
<Recommendations /> {/* 3s 完成 */}
</Suspense>
</div>
)
}
用户先看到收入模块,然后订单,最后推荐——不用等最慢的。
嵌套 Suspense
可以创建分层加载体验:
export default async function ProductPage({ params }) {
const { id } = await params
return (
<div>
<h1>产品详情</h1>
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails id={id} />
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews productId={id} />
</Suspense>
</Suspense>
</div>
)
}
外层加载完显示产品详情,内层继续加载评论。
loading.tsx vs Suspense
loading.tsx | <Suspense> | |
|---|---|---|
| 范围 | 整页 | 任意组件 |
| 设置 | 放文件 | 包裹组件 |
| 预取 | 自动预取 | 默认不预取 |
| 适用场景 | 页面没数据就没内容 | 大部分页面 |
推荐:优先用 <Suspense>,把 fallback 放在需要它的组件旁边。
流式渲染的两个流
HTML 流
React 服务端渲染器生成渐进式 HTML 块:
- 静态部分(布局、导航、fallback)先发送
- 异步组件完成后,HTML 和内联
<script>一起发送 - 浏览器立即执行替换,无需等 JS 包加载
组件负载(RSC Payload)
组件负载是组件树的序列化表示,用于:
- 首次加载:注水(hydrate)页面
- 客户端导航:只获取 RSC Payload,不传 HTML
静态 shell
在任何异步工作之前渲染的内容叫静态 shell:
- 布局
- 导航
- Suspense fallback
- 缓存的组件(
use cache)
静态 shell 立即发送给用户,提供即时视觉反馈。
把动态访问往下推
最大化静态 shell 的关键:把动态数据访问推迟到真正需要的组件。
// ❌ 不好:layout 里 await cookies,整个布局变成动态
export default async function Layout({ children }) {
const cookieStore = await cookies()
const theme = cookieStore.get('theme')?.value
return (
<div data-theme={theme}>
<nav>...</nav>
{children}
</div>
)
}
// ✅ 好:只把需要 cookies 的部分包在 Suspense 里
export default function Layout({ children }) {
const cookieStore = cookies() // 不 await
return (
<div>
<nav>...</nav>
<Suspense fallback={<div>加载中...</div>}>
<UserMenu cookiePromise={cookieStore} />
</Suspense>
{children}
</div>
)
}
同样的原则适用于 params、searchParams 和数据获取。
设计有意义的加载态
好的加载态不是转圈圈,而是让用户知道:
- 应用正在响应
- 即将显示什么内容
- 大概还要多久
Skeleton 模式
骨架屏模拟最终内容的形状:
export default function CardSkeleton() {
return (
<div className="animate-pulse rounded border p-4">
<div className="h-48 bg-gray-200 rounded mb-4" />
<div className="h-6 w-3/4 bg-gray-200 rounded mb-2" />
<div className="h-4 w-full bg-gray-200 rounded mb-2" />
<div className="h-4 w-2/3 bg-gray-200 rounded" />
</div>
)
}
关键:尺寸匹配
骨架屏的尺寸应该和最终内容一致,避免布局抖动(CLS)。
流式渲染与 Web Vitals
TTFB(首字节时间)
流式渲染让 TTFB 大幅降低——服务器一有静态 shell 就发送,不用等数据。
FCP(首次内容绘制)
静态 shell 立即渲染,FCP 和数据获取时间解耦。
LCP(最大内容绘制)
如果 LCP 元素在 Suspense 边界内,它要等数据加载完才显示。保持 LCP 元素在边界外。
CLS(累积布局偏移)
骨架屏和最终内容尺寸不一致会导致布局偏移。用固定高度或 min-height 预留空间。
HTTP 契约
流式渲染开始后,HTTP 响应头(包括状态码)已经发送。无法再修改状态码。
notFound()在流式渲染中:注入<meta name="robots" content="noindex">redirect()在流式渲染中:变成客户端跳转
要获取真正的 404 状态码,在流式渲染开始前检查:
export default async function PostPage({ params }) {
const { slug } = await params
const exists = await checkSlugExists(slug)
if (!exists) notFound() // 在 Suspense 之前
return (
<Suspense fallback={<p>加载中...</p>}>
<PostContent slug={slug} />
</Suspense>
)
}
影响流式渲染的因素
- 反向代理:Nginx 默认缓冲,需设置
X-Accel-Buffering: no - CDN:部分 CDN 缓冲整个响应
- Serverless:AWS Lambda 需开启 response streaming
- 压缩:Gzip/Brotli 可能缓冲
- Safari:缓冲前 1024 字节
小结
这一章我们学了加载态和流式渲染:
loading.tsx创建整页加载态,自动预取<Suspense>创建细粒度加载态- 流式渲染让页面分段显示,提升感知性能
- 静态 shell 包含布局、导航、fallback
- 把动态访问往下推,最大化静态 shell
- 骨架屏尺寸匹配,避免布局偏移
恭喜!你已经完成了基础入门部分的学习。接下来可以继续学习核心进阶章节。