Next.js 流式渲染 🌊
🎯 概述
流式渲染(Streaming)允许服务器逐步发送 HTML 内容到客户端,而不是等待整个页面渲染完成。这大大改善了首屏加载时间和用户体验。
📦 什么是流式渲染?
工作原理
- 服务器开始渲染页面
- 立即发送已渲染的部分
- 继续渲染剩余内容
- 逐步发送到客户端
- 客户端逐步显示内容
优势
- ⚡ 更快的首字节时间 (TTFB)
- 👀 更快的首次内容绘制 (FCP)
- 🎯 渐进式页面加载
- 💪 更好的用户体验
- 📱 适合慢速网络
🚀 使用 Suspense
基本用法
tsx
// app/page.tsx
import { Suspense } from 'react'
async function SlowComponent() {
await new Promise(resolve => setTimeout(resolve, 3000))
return <div>慢速内容</div>
}
export default function Page() {
return (
<div>
<h1>快速内容</h1>
<Suspense fallback={<div>加载中...</div>}>
<SlowComponent />
</Suspense>
</div>
)
}多个 Suspense 边界
tsx
// app/dashboard/page.tsx
import { Suspense } from 'react'
async function UserInfo() {
const user = await fetchUser()
return <div>{user.name}</div>
}
async function Posts() {
const posts = await fetchPosts()
return <div>{posts.length} 篇文章</div>
}
async function Analytics() {
const data = await fetchAnalytics()
return <div>{data.views} 次浏览</div>
}
export default function Dashboard() {
return (
<div>
<h1>仪表板</h1>
<Suspense fallback={<div>加载用户信息...</div>}>
<UserInfo />
</Suspense>
<Suspense fallback={<div>加载文章...</div>}>
<Posts />
</Suspense>
<Suspense fallback={<div>加载统计...</div>}>
<Analytics />
</Suspense>
</div>
)
}📝 嵌套 Suspense
层级加载
tsx
// app/blog/page.tsx
import { Suspense } from 'react'
async function BlogList() {
const posts = await fetchPosts()
return (
<div>
{posts.map(post => (
<Suspense key={post.id} fallback={<PostSkeleton />}>
<BlogPost id={post.id} />
</Suspense>
))}
</div>
)
}
async function BlogPost({ id }) {
const post = await fetchPost(id)
return (
<article>
<h2>{post.title}</h2>
<Suspense fallback={<div>加载评论...</div>}>
<Comments postId={id} />
</Suspense>
</article>
)
}
async function Comments({ postId }) {
const comments = await fetchComments(postId)
return <div>{comments.length} 条评论</div>
}
export default function BlogPage() {
return (
<Suspense fallback={<div>加载博客列表...</div>}>
<BlogList />
</Suspense>
)
}🎨 加载骨架屏
创建骨架组件
tsx
// components/Skeleton.tsx
export function PostSkeleton() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-3/4 mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-full mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-5/6"></div>
</div>
)
}
export function UserSkeleton() {
return (
<div className="animate-pulse flex items-center">
<div className="h-12 w-12 bg-gray-200 rounded-full"></div>
<div className="ml-4">
<div className="h-4 bg-gray-200 rounded w-24 mb-2"></div>
<div className="h-3 bg-gray-200 rounded w-32"></div>
</div>
</div>
)
}使用骨架屏
tsx
// app/profile/page.tsx
import { Suspense } from 'react'
import { UserSkeleton, PostSkeleton } from '@/components/Skeleton'
async function UserProfile() {
const user = await fetchUser()
return (
<div>
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.bio}</p>
</div>
)
}
async function UserPosts() {
const posts = await fetchUserPosts()
return (
<div>
{posts.map(post => (
<article key={post.id}>{post.title}</article>
))}
</div>
)
}
export default function ProfilePage() {
return (
<div>
<Suspense fallback={<UserSkeleton />}>
<UserProfile />
</Suspense>
<Suspense fallback={<PostSkeleton />}>
<UserPosts />
</Suspense>
</div>
)
}🔄 loading.tsx 文件
路由级加载状态
tsx
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900"></div>
</div>
)
}
// app/dashboard/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export default async function Dashboard() {
const data = await getData()
return <div>{data.title}</div>
}嵌套加载状态
tsx
// app/blog/loading.tsx
export default function BlogLoading() {
return <div>加载博客...</div>
}
// app/blog/[slug]/loading.tsx
export default function PostLoading() {
return <div>加载文章...</div>
}📊 实战示例
电商产品页面
tsx
// app/products/[id]/page.tsx
import { Suspense } from 'react'
async function ProductInfo({ id }) {
const product = await fetchProduct(id)
return (
<div>
<h1>{product.name}</h1>
<p>{product.price}元</p>
<p>{product.description}</p>
</div>
)
}
async function ProductImages({ id }) {
const images = await fetchProductImages(id)
return (
<div className="grid grid-cols-2 gap-4">
{images.map(img => (
<img key={img.id} src={img.url} alt="" />
))}
</div>
)
}
async function ProductReviews({ id }) {
const reviews = await fetchReviews(id)
return (
<div>
<h2>用户评价</h2>
{reviews.map(review => (
<div key={review.id}>
<p>{review.rating} 星</p>
<p>{review.comment}</p>
</div>
))}
</div>
)
}
async function RelatedProducts({ id }) {
const products = await fetchRelatedProducts(id)
return (
<div>
<h2>相关产品</h2>
<div className="grid grid-cols-4 gap-4">
{products.map(p => (
<div key={p.id}>{p.name}</div>
))}
</div>
</div>
)
}
export default function ProductPage({ params }) {
return (
<div>
<Suspense fallback={<ProductInfoSkeleton />}>
<ProductInfo id={params.id} />
</Suspense>
<Suspense fallback={<ImagesSkeleton />}>
<ProductImages id={params.id} />
</Suspense>
<Suspense fallback={<div>加载评价...</div>}>
<ProductReviews id={params.id} />
</Suspense>
<Suspense fallback={<div>加载相关产品...</div>}>
<RelatedProducts id={params.id} />
</Suspense>
</div>
)
}社交媒体动态
tsx
// app/feed/page.tsx
import { Suspense } from 'react'
async function FeedItem({ id }) {
const post = await fetchPost(id)
return (
<article>
<h3>{post.title}</h3>
<p>{post.content}</p>
<Suspense fallback={<div>加载评论...</div>}>
<Comments postId={id} />
</Suspense>
</article>
)
}
async function Comments({ postId }) {
const comments = await fetchComments(postId)
return (
<div>
{comments.map(comment => (
<div key={comment.id}>{comment.text}</div>
))}
</div>
)
}
async function Feed() {
const postIds = await fetchFeedIds()
return (
<div>
{postIds.map(id => (
<Suspense key={id} fallback={<FeedItemSkeleton />}>
<FeedItem id={id} />
</Suspense>
))}
</div>
)
}
export default function FeedPage() {
return (
<div>
<h1>动态</h1>
<Suspense fallback={<div>加载动态...</div>}>
<Feed />
</Suspense>
</div>
)
}📚 最佳实践
1. 合理划分 Suspense 边界
tsx
// ✅ 推荐:独立的 Suspense 边界
<div>
<Suspense fallback={<HeaderSkeleton />}>
<Header />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<Content />
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
</div>
// ❌ 不推荐:单个 Suspense 包裹所有内容
<Suspense fallback={<PageSkeleton />}>
<Header />
<Content />
<Sidebar />
</Suspense>2. 优先加载关键内容
tsx
export default function Page() {
return (
<div>
{/* 关键内容:立即渲染 */}
<h1>页面标题</h1>
<p>重要描述</p>
{/* 次要内容:延迟加载 */}
<Suspense fallback={<Skeleton />}>
<SecondaryContent />
</Suspense>
</div>
)
}3. 提供有意义的加载状态
tsx
// ✅ 推荐:具体的加载提示
<Suspense fallback={<div>正在加载用户评论...</div>}>
<Comments />
</Suspense>
// ❌ 不推荐:通用的加载提示
<Suspense fallback={<div>加载中...</div>}>
<Comments />
</Suspense>4. 使用骨架屏而非加载动画
tsx
// ✅ 推荐:骨架屏
<Suspense fallback={<ArticleSkeleton />}>
<Article />
</Suspense>
// ❌ 不推荐:简单的加载动画
<Suspense fallback={<Spinner />}>
<Article />
</Suspense>🔗 相关资源
下一步:学习 Next.js 状态管理。