跳到主要内容

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.jsReact最成熟、功能最全
NuxtVueVue 官方推荐
Astro多框架默认零 JS
RemixReact专注服务端
SvelteKitSvelte轻量高性能

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 的区别?

答案

特性SSRSSG
渲染时机每次请求构建时
数据新鲜度实时构建时确定
TTFB较高极低
服务器要求需要 Node.js静态托管
适用场景个性化/动态内容博客/文档/营销页

Q2: 什么是 Hydration?有什么问题?

答案

Hydration 是将服务端渲染的静态 HTML 与 React 应用关联的过程,使页面变得可交互。

问题

  1. TTI 延迟:需要下载和执行 JS
  2. 闪烁:服务端和客户端不一致
  3. 内存占用:需要重新构建虚拟 DOM

优化方案

  1. Streaming SSR:分块发送 HTML
  2. Partial Hydration:只 Hydrate 交互部分
  3. Progressive Hydration:延迟非关键部分
  4. 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

相关链接