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 的共同作用:
- 提升感知性能:用户立即看到页面结构,而不是空白页面
- 优化 LCP:关键内容优先渲染,减少最大内容绘制时间
- 减少 CLS:使用骨架屏避免布局偏移
- 改善用户体验:渐进式加载,用户感觉页面响应更快
- 提高 SEO:搜索引擎可以更快地抓取页面内容
这些技术让 Next.js 应用能够提供更好的性能和用户体验,特别是在处理慢速数据获取时。