#Next.js Streaming
#Overview
Streaming allows the server to send HTML content to the client incrementally, rather than waiting for the entire page to finish rendering. This greatly improves first-screen load time and user experience.
#What is Streaming?
#How It Works
- The server begins rendering the page
- Already-rendered parts are sent immediately
- Remaining content continues to render
- Content is sent incrementally to the client
- The client displays content progressively
#Advantages
- Faster Time to First Byte (TTFB)
- Faster First Contentful Paint (FCP)
- Progressive page loading
- Better user experience
- Suitable for slow networks
#Using Suspense
#Basic Usage
// 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>
)
}#Multiple Suspense Boundaries
// 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>
)
}#Nested Suspense
#Hierarchical Loading
// 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>
)
}#Loading Skeletons
#Creating Skeleton Components
// 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>
)
}#Using Skeletons
// 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 File
#Route-Level Loading State
// 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>
}#Nested Loading States
// app/blog/loading.tsx
export default function BlogLoading() {
return <div>加载博客...</div>
}
// app/blog/[slug]/loading.tsx
export default function PostLoading() {
return <div>加载文章...</div>
}#Practical Examples
#E-Commerce Product Page
// 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>
)
}#Social Media Feed
// 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>
)
}#Best Practices
#1. Define Suspense Boundaries Thoughtfully
// ✅ 推荐:独立的 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. Prioritize Critical Content
export default function Page() {
return (
<div>
{/* 关键内容:立即渲染 */}
<h1>页面标题</h1>
<p>重要描述</p>
{/* 次要内容:延迟加载 */}
<Suspense fallback={<Skeleton />}>
<SecondaryContent />
</Suspense>
</div>
)
}#3. Provide Meaningful Loading States
// ✅ 推荐:具体的加载提示
<Suspense fallback={<div>正在加载用户评论...</div>}>
<Comments />
</Suspense>
// ❌ 不推荐:通用的加载提示
<Suspense fallback={<div>加载中...</div>}>
<Comments />
</Suspense>#4. Use Skeletons Instead of Loading Spinners
// ✅ 推荐:骨架屏
<Suspense fallback={<ArticleSkeleton />}>
<Article />
</Suspense>
// ❌ 不推荐:简单的加载动画
<Suspense fallback={<Spinner />}>
<Article />
</Suspense>#Related Resources
Previous Chapter: Client-Side Rendering (CSR) | Next Chapter: State Management