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

  1. The server begins rendering the page
  2. Already-rendered parts are sent immediately
  3. Remaining content continues to render
  4. Content is sent incrementally to the client
  5. 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>

Previous Chapter: Client-Side Rendering (CSR) | Next Chapter: State Management