SSR 与 SSG
问题
什么是服务端渲染(SSR)和静态站点生成(SSG)?它们的优缺点是什么?如何选择合适的渲染方式?
答案
SSR 和 SSG 是优化首屏性能和 SEO 的核心技术。选择合适的渲染策略对应用性能和用户体验至关重要。
渲染方式对比
| 方式 | 渲染时机 | 首屏速度 | SEO | 服务器压力 | 适用场景 |
|---|---|---|---|---|---|
| CSR | 浏览器 | 慢 | 差 | 低 | 后台管理系统 |
| SSR | 每次请求 | 快 | 好 | 高 | 动态内容 |
| SSG | 构建时 | 最快 | 好 | 最低 | 静态内容 |
| ISR | 构建+增量 | 快 | 好 | 中等 | 大量静态页 |
CSR(客户端渲染)
<!-- CSR 的初始 HTML -->
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="root"></div>
<script src="/bundle.js"></script>
</body>
</html>
CSR 优缺点
| 优点 | 缺点 |
|---|---|
| 服务器压力小 | 首屏慢 |
| 交互体验好 | SEO 差 |
| 部署简单 | 依赖 JavaScript |
| 前后端分离 | 白屏时间长 |
SSR(服务端渲染)
Next.js SSR 示例
// pages/products/[id].tsx
import { GetServerSideProps } from 'next';
interface Product {
id: string;
name: string;
price: number;
}
interface Props {
product: Product;
}
export default function ProductPage({ product }: Props) {
return (
<div>
<h1>{product.name}</h1>
<p>价格: ¥{product.price}</p>
</div>
);
}
// 每次请求时执行
export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
const { id } = context.params!;
const response = await fetch(`https://api.example.com/products/${id}`);
const product = await response.json();
if (!product) {
return { notFound: true };
}
return {
props: { product },
};
};
Vue/Nuxt SSR 示例
<!-- pages/products/[id].vue -->
<template>
<div>
<h1>{{ product.name }}</h1>
<p>价格: ¥{{ product.price }}</p>
</div>
</template>
<script setup lang="ts">
const route = useRoute();
// 在服务端获取数据
const { data: product } = await useFetch(
() => `https://api.example.com/products/${route.params.id}`
);
if (!product.value) {
throw createError({ statusCode: 404, message: 'Product not found' });
}
</script>
SSR 优缺点
| 优点 | 缺点 |
|---|---|
| 首屏快 | 服务器压力大 |
| SEO 友好 | TTFB 较高 |
| 更好的 LCP | 需要 Node.js 服务器 |
| 社交分享友好 | Hydration 成本 |
SSG(静态站点生成)
Next.js SSG 示例
// pages/posts/[slug].tsx
import { GetStaticPaths, GetStaticProps } from 'next';
interface Post {
slug: string;
title: string;
content: string;
}
interface Props {
post: Post;
}
export default function PostPage({ post }: Props) {
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// 构建时生成所有路径
export const getStaticPaths: GetStaticPaths = async () => {
const posts = await getAllPosts();
return {
paths: posts.map(post => ({
params: { slug: post.slug },
})),
fallback: false, // 其他路径返回 404
};
};
// 构建时获取数据
export const getStaticProps: GetStaticProps<Props> = async (context) => {
const { slug } = context.params!;
const post = await getPostBySlug(slug as string);
return {
props: { post },
};
};
SSG 优缺点
| 优点 | 缺点 |
|---|---|
| 最快的首屏 | 构建时间长 |
| CDN 分发 | 内容更新需重新构建 |
| 服务器零压力 | 不适合动态内容 |
| 高可用性 | 大量页面构建慢 |
ISR(增量静态再生)
ISR 结合了 SSG 和 SSR 的优点:静态生成 + 后台增量更新。
Next.js ISR 示例
// pages/products/[id].tsx
export const getStaticProps: GetStaticProps = async ({ params }) => {
const product = await getProduct(params!.id as string);
return {
props: { product },
// 60 秒后,下次请求触发后台重新生成
revalidate: 60,
};
};
export const getStaticPaths: GetStaticPaths = async () => {
// 只预生成热门商品
const popularProducts = await getPopularProducts();
return {
paths: popularProducts.map(p => ({
params: { id: p.id },
})),
// 其他页面首次访问时生成
fallback: 'blocking',
};
};
fallback 选项
| 值 | 行为 |
|---|---|
false | 未预生成的路径返回 404 |
true | 先显示 fallback,生成后替换 |
'blocking' | 等待生成完成,类似 SSR |
// fallback: true 时的处理
function ProductPage({ product }) {
const router = useRouter();
// 正在生成页面
if (router.isFallback) {
return <LoadingSkeleton />;
}
return <ProductDetail product={product} />;
}
按需 ISR(On-Demand ISR)
Next.js 12.1+ 支持按需触发重新验证。
// pages/api/revalidate.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
// 验证密钥
if (req.query.secret !== process.env.REVALIDATE_SECRET) {
return res.status(401).json({ message: 'Invalid token' });
}
try {
const { path } = req.body;
// 重新生成指定页面
await res.revalidate(path);
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).json({ message: 'Error revalidating' });
}
}
// CMS Webhook 调用
// POST /api/revalidate?secret=xxx
// Body: { "path": "/posts/hello-world" }
Hydration 优化
Hydration 是 SSR/SSG 页面在客户端"激活"的过程。
Streaming SSR
// Next.js 13+ App Router
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<Header />
<Suspense fallback={<ProductsSkeleton />}>
<Products />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews />
</Suspense>
</div>
);
}
// 异步组件
async function Products() {
const products = await fetchProducts();
return <ProductList products={products} />;
}
async function Reviews() {
const reviews = await fetchReviews();
return <ReviewList reviews={reviews} />;
}
Partial Hydration
// 使用 React Server Components (RSC)
// 服务端组件,不会发送到客户端
async function ServerOnlyComponent() {
const data = await db.query('SELECT * FROM products');
return <ProductList data={data} />;
}
// 客户端组件,需要 Hydration
'use client';
function ClientComponent() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
框架选择
| 框架 | 生态 | 特点 |
|---|---|---|
| Next.js | React | 最成熟、功能最全 |
| Nuxt | Vue | Vue 官方推荐 |
| Astro | 多框架 | 默认零 JS |
| Remix | React | 专注服务端 |
| SvelteKit | Svelte | 轻量高性能 |
Next.js 渲染方式选择
// 1. Server Component - 默认,服务端渲染
async function Page() {
const data = await fetch('...');
return <div>{data}</div>;
}
// 2. Client Component - 客户端交互
'use client';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c + 1)}>{count}</button>;
}
// 3. Static - 构建时生成,可配置 revalidate
export const revalidate = 3600; // 1 小时
// 4. Dynamic - 每次请求都执行
export const dynamic = 'force-dynamic';
常见面试问题
Q1: SSR 和 SSG 的区别?
答案:
| 特性 | SSR | SSG |
|---|---|---|
| 渲染时机 | 每次请求 | 构建时 |
| 数据新鲜度 | 实时 | 构建时确定 |
| TTFB | 较高 | 极低 |
| 服务器要求 | 需要 Node.js | 静态托管 |
| 适用场景 | 个性化/动态内容 | 博客/文档/营销页 |
Q2: 什么是 Hydration?有什么问题?
答案:
Hydration 是将服务端渲染的静态 HTML 与 React 应用关联的过程,使页面变得可交互。
问题:
- TTI 延迟:需要下载和执行 JS
- 闪烁:服务端和客户端不一致
- 内存占用:需要重新构建虚拟 DOM
优化方案:
- Streaming SSR:分块发送 HTML
- Partial Hydration:只 Hydrate 交互部分
- Progressive Hydration:延迟非关键部分
- React Server Components:减少客户端 JS
Q3: 如何选择渲染方式?
答案:
| 场景 | 推荐方式 |
|---|---|
| 博客/文档 | SSG |
| 电商商品页 | ISR |
| 用户仪表盘 | CSR |
| 实时数据 | SSR |
| 新闻首页 | ISR/SSR |
Q4: ISR 的工作原理?
答案:
// 1. 构建时生成部分页面
export const getStaticPaths = async () => ({
paths: ['/post/1', '/post/2'],
fallback: 'blocking', // 首次访问其他路径时 SSR
});
// 2. 设置重新验证时间
export const getStaticProps = async () => ({
props: { data },
revalidate: 60, // 60秒后重新生成
});
// 3. 工作流程
// - 60s 内:返回缓存页面
// - 60s 后首次请求:返回旧页面 + 后台重新生成
// - 下次请求:返回新页面
Q5: 如何处理 SSR 的性能问题?
答案:
// 1. 缓存
// 使用 Redis/内存缓存渲染结果
const cached = await redis.get(cacheKey);
if (cached) return { props: JSON.parse(cached) };
// 2. Streaming
// React 18 + Next.js 13
import { Suspense } from 'react';
<Suspense fallback={<Loading />}>
<SlowComponent />
</Suspense>
// 3. 边缘渲染
// Vercel Edge Functions / Cloudflare Workers
export const runtime = 'edge';
// 4. 数据预取优化
// 并行请求、减少瀑布流
const [user, posts] = await Promise.all([
getUser(id),
getPosts(id),
]);
// 5. 组件级缓存
// 使用 React.cache() 或 unstable_cache