Q18: 什么是流式传输(Streaming)和 Suspense?它们如何共同作用来提升 perceived performance(感知性能)和 Core

48 阅读3分钟

Next.js 面试题详细答案 - Q18

Q18: 什么是流式传输(Streaming)和 Suspense?它们如何共同作用来提升 perceived performance(感知性能)和 Core Web Vitals(特别是 LCP)?

流式传输(Streaming)概述

流式传输是一种技术,允许服务器在渲染完成之前就开始向客户端发送 HTML 内容,而不是等待整个页面渲染完成。

Suspense 组件

1. 基本用法
import { Suspense } from 'react'

function App() {
  return (
    <div>
      <h1>我的应用</h1>
      <Suspense fallback={<div>加载中...</div>}>
        <SlowComponent />
      </Suspense>
    </div>
  )
}

async function SlowComponent() {
  // 模拟慢速数据获取
  await new Promise((resolve) => setTimeout(resolve, 2000))
  return <div>慢速组件内容</div>
}
2. 多个 Suspense 边界
import { Suspense } from 'react'

function BlogPage() {
  return (
    <div>
      <h1>博客页面</h1>

      <Suspense fallback={<div>加载文章列表...</div>}>
        <BlogList />
      </Suspense>

      <Suspense fallback={<div>加载推荐文章...</div>}>
        <RecommendedPosts />
      </Suspense>

      <Suspense fallback={<div>加载评论...</div>}>
        <Comments />
      </Suspense>
    </div>
  )
}

async function BlogList() {
  const posts = await fetch('/api/posts')
  const data = await posts.json()

  return (
    <ul>
      {data.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

async function RecommendedPosts() {
  const posts = await fetch('/api/recommended')
  const data = await posts.json()

  return (
    <div>
      <h3>推荐文章</h3>
      {data.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  )
}

async function Comments() {
  const comments = await fetch('/api/comments')
  const data = await comments.json()

  return (
    <div>
      <h3>评论</h3>
      {data.map((comment) => (
        <div key={comment.id}>{comment.text}</div>
      ))}
    </div>
  )
}

流式传输实现

1. 服务端流式传输
// app/blog/page.js
import { Suspense } from 'react'

export default function BlogPage() {
  return (
    <div>
      <h1>博客</h1>

      {/* 立即渲染的静态内容 */}
      <div className="hero">
        <h2>欢迎来到我们的博客</h2>
        <p>这里有很多有趣的文章等着你</p>
      </div>

      {/* 流式传输的动态内容 */}
      <Suspense fallback={<BlogListSkeleton />}>
        <BlogList />
      </Suspense>

      <Suspense fallback={<RecommendedSkeleton />}>
        <RecommendedPosts />
      </Suspense>
    </div>
  )
}

async function BlogList() {
  const posts = await fetch('/api/posts', {
    next: { revalidate: 3600 },
  })
  const data = await posts.json()

  return (
    <section>
      <h2>最新文章</h2>
      <div className="posts-grid">
        {data.map((post) => (
          <article key={post.id} className="post-card">
            <h3>{post.title}</h3>
            <p>{post.excerpt}</p>
            <time>{post.publishedAt}</time>
          </article>
        ))}
      </div>
    </section>
  )
}

async function RecommendedPosts() {
  const posts = await fetch('/api/recommended', {
    next: { revalidate: 1800 },
  })
  const data = await posts.json()

  return (
    <section>
      <h2>推荐阅读</h2>
      <div className="recommended-list">
        {data.map((post) => (
          <div key={post.id} className="recommended-item">
            <h4>{post.title}</h4>
            <p>{post.excerpt}</p>
          </div>
        ))}
      </div>
    </section>
  )
}

// 骨架屏组件
function BlogListSkeleton() {
  return (
    <section>
      <h2>最新文章</h2>
      <div className="posts-grid">
        {Array.from({ length: 6 }).map((_, i) => (
          <div key={i} className="post-card skeleton">
            <div className="skeleton-line"></div>
            <div className="skeleton-line"></div>
            <div className="skeleton-line short"></div>
          </div>
        ))}
      </div>
    </section>
  )
}

function RecommendedSkeleton() {
  return (
    <section>
      <h2>推荐阅读</h2>
      <div className="recommended-list">
        {Array.from({ length: 3 }).map((_, i) => (
          <div key={i} className="recommended-item skeleton">
            <div className="skeleton-line"></div>
            <div className="skeleton-line short"></div>
          </div>
        ))}
      </div>
    </section>
  )
}
2. 客户端流式传输
'use client'
import { Suspense, useState, useEffect } from 'react'

function ClientStreamingExample() {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    // 模拟流式数据获取
    const fetchData = async () => {
      const response = await fetch('/api/streaming-data')
      const reader = response.body.getReader()
      const decoder = new TextDecoder()

      let result = ''
      while (true) {
        const { done, value } = await reader.read()
        if (done) break

        result += decoder.decode(value, { stream: true })

        // 逐步更新数据
        try {
          const parsed = JSON.parse(result)
          setData(parsed)
        } catch (e) {
          // 继续等待更多数据
        }
      }

      setLoading(false)
    }

    fetchData()
  }, [])

  if (loading) {
    return (
      <div>
        <h1>加载中...</h1>
        <div className="loading-bar"></div>
      </div>
    )
  }

  return (
    <div>
      <h1>流式数据</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  )
}

性能优化策略

1. 关键内容优先
// app/product/[id]/page.js
import { Suspense } from 'react'

export default function ProductPage({ params }) {
  return (
    <div>
      {/* 关键内容立即渲染 */}
      <header>
        <h1>产品详情</h1>
        <nav>
          <a href="/">首页</a>
          <a href="/products">产品列表</a>
        </nav>
      </header>

      {/* 产品基本信息 */}
      <Suspense fallback={<ProductInfoSkeleton />}>
        <ProductInfo productId={params.id} />
      </Suspense>

      {/* 次要内容 */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews productId={params.id} />
      </Suspense>

      <Suspense fallback={<RelatedSkeleton />}>
        <RelatedProducts productId={params.id} />
      </Suspense>
    </div>
  )
}

async function ProductInfo({ productId }) {
  const product = await fetch(`/api/products/${productId}`)
  const data = await product.json()

  return (
    <section className="product-info">
      <h2>{data.name}</h2>
      <p className="price">${data.price}</p>
      <p>{data.description}</p>
      <button>加入购物车</button>
    </section>
  )
}

async function ProductReviews({ productId }) {
  const reviews = await fetch(`/api/products/${productId}/reviews`)
  const data = await reviews.json()

  return (
    <section className="reviews">
      <h3>用户评价</h3>
      {data.map((review) => (
        <div key={review.id} className="review">
          <h4>{review.title}</h4>
          <p>{review.content}</p>
          <div className="rating">{'★'.repeat(review.rating)}</div>
        </div>
      ))}
    </section>
  )
}

async function RelatedProducts({ productId }) {
  const products = await fetch(`/api/products/${productId}/related`)
  const data = await products.json()

  return (
    <section className="related-products">
      <h3>相关产品</h3>
      <div className="products-grid">
        {data.map((product) => (
          <div key={product.id} className="product-card">
            <h4>{product.name}</h4>
            <p>${product.price}</p>
          </div>
        ))}
      </div>
    </section>
  )
}
2. 渐进式加载
// app/dashboard/page.js
import { Suspense } from 'react'

export default function Dashboard() {
  return (
    <div className="dashboard">
      <header>
        <h1>仪表盘</h1>
        <nav>
          <a href="/dashboard">概览</a>
          <a href="/dashboard/analytics">分析</a>
          <a href="/dashboard/settings">设置</a>
        </nav>
      </header>

      <main>
        {/* 第一优先级:关键指标 */}
        <Suspense fallback={<MetricsSkeleton />}>
          <KeyMetrics />
        </Suspense>

        {/* 第二优先级:图表 */}
        <Suspense fallback={<ChartsSkeleton />}>
          <Charts />
        </Suspense>

        {/* 第三优先级:详细数据 */}
        <Suspense fallback={<DataSkeleton />}>
          <DetailedData />
        </Suspense>
      </main>
    </div>
  )
}

async function KeyMetrics() {
  const metrics = await fetch('/api/metrics')
  const data = await metrics.json()

  return (
    <section className="metrics">
      <div className="metric-card">
        <h3>总用户数</h3>
        <p className="metric-value">{data.totalUsers}</p>
      </div>
      <div className="metric-card">
        <h3>活跃用户</h3>
        <p className="metric-value">{data.activeUsers}</p>
      </div>
      <div className="metric-card">
        <h3>收入</h3>
        <p className="metric-value">${data.revenue}</p>
      </div>
    </section>
  )
}

async function Charts() {
  const chartData = await fetch('/api/charts')
  const data = await chartData.json()

  return (
    <section className="charts">
      <h3>数据图表</h3>
      <div className="chart-container">
        {/* 图表组件 */}
        <Chart data={data} />
      </div>
    </section>
  )
}

async function DetailedData() {
  const data = await fetch('/api/detailed-data')
  const result = await data.json()

  return (
    <section className="detailed-data">
      <h3>详细数据</h3>
      <table>
        <thead>
          <tr>
            <th>日期</th>
            <th>用户数</th>
            <th>收入</th>
          </tr>
        </thead>
        <tbody>
          {result.map((row) => (
            <tr key={row.date}>
              <td>{row.date}</td>
              <td>{row.users}</td>
              <td>${row.revenue}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </section>
  )
}

Core Web Vitals 优化

1. LCP (Largest Contentful Paint) 优化
// app/page.js
import { Suspense } from 'react'

export default function HomePage() {
  return (
    <div>
      {/* 关键内容立即渲染,优化 LCP */}
      <header className="hero">
        <h1>欢迎来到我们的网站</h1>
        <p>这是最重要的内容,会立即显示</p>
        <button>开始使用</button>
      </header>

      {/* 次要内容流式加载 */}
      <Suspense fallback={<div>加载中...</div>}>
        <SecondaryContent />
      </Suspense>
    </div>
  )
}

async function SecondaryContent() {
  const data = await fetch('/api/secondary-content')
  const result = await data.json()

  return (
    <section>
      <h2>次要内容</h2>
      <p>{result.content}</p>
    </section>
  )
}
2. CLS (Cumulative Layout Shift) 优化
// app/blog/page.js
import { Suspense } from 'react'

export default function BlogPage() {
  return (
    <div>
      <h1>博客</h1>

      {/* 使用固定高度的骨架屏避免布局偏移 */}
      <Suspense fallback={<BlogListSkeleton />}>
        <BlogList />
      </Suspense>
    </div>
  )
}

function BlogListSkeleton() {
  return (
    <div className="blog-list-skeleton">
      {Array.from({ length: 6 }).map((_, i) => (
        <div key={i} className="blog-item-skeleton">
          <div className="skeleton-image"></div>
          <div className="skeleton-content">
            <div className="skeleton-line"></div>
            <div className="skeleton-line"></div>
            <div className="skeleton-line short"></div>
          </div>
        </div>
      ))}
    </div>
  )
}

async function BlogList() {
  const posts = await fetch('/api/posts')
  const data = await posts.json()

  return (
    <div className="blog-list">
      {data.map((post) => (
        <article key={post.id} className="blog-item">
          <img src={post.image} alt={post.title} />
          <div className="content">
            <h3>{post.title}</h3>
            <p>{post.excerpt}</p>
            <time>{post.publishedAt}</time>
          </div>
        </article>
      ))}
    </div>
  )
}

实际应用示例

1. 电商产品页面
// app/products/[id]/page.js
import { Suspense } from 'react'

export default function ProductPage({ params }) {
  return (
    <div className="product-page">
      {/* 立即显示的关键信息 */}
      <header>
        <h1>产品详情</h1>
        <nav>
          <a href="/">首页</a>
          <a href="/products">产品列表</a>
        </nav>
      </header>

      {/* 产品基本信息 */}
      <Suspense fallback={<ProductInfoSkeleton />}>
        <ProductInfo productId={params.id} />
      </Suspense>

      {/* 产品图片 */}
      <Suspense fallback={<ProductImagesSkeleton />}>
        <ProductImages productId={params.id} />
      </Suspense>

      {/* 用户评价 */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews productId={params.id} />
      </Suspense>

      {/* 相关产品 */}
      <Suspense fallback={<RelatedSkeleton />}>
        <RelatedProducts productId={params.id} />
      </Suspense>
    </div>
  )
}

总结

流式传输和 Suspense 的共同作用:

  1. 提升感知性能:用户立即看到页面结构,而不是空白页面
  2. 优化 LCP:关键内容优先渲染,减少最大内容绘制时间
  3. 减少 CLS:使用骨架屏避免布局偏移
  4. 改善用户体验:渐进式加载,用户感觉页面响应更快
  5. 提高 SEO:搜索引擎可以更快地抓取页面内容

这些技术让 Next.js 应用能够提供更好的性能和用户体验,特别是在处理慢速数据获取时。