SSR vs CSR

21 分钟

一、从一个白屏问题说起

打开一个 React SPA 应用,查看页面源代码,你大概率只会看到这样的 HTML:

<!DOCTYPE html>
<html>
  <head><title>My App</title></head>
  <body>
    <div id="root"></div>
    <script src="/bundle.js"></script>
  </body>
</html>

浏览器拿到的是一个空的 <div id="root">,所有内容都要等 JavaScript 下载、解析、执行完毕后才能渲染出来。在这段时间里,用户看到的就是白屏。这就是 CSR(Client-Side Rendering)的典型问题。

SSR(Server-Side Rendering)则是另一种思路:服务器直接返回填充好内容的 HTML,浏览器拿到就能渲染,不用等 JS 执行完

这两种渲染模式各有适用场景,理解它们的原理和取舍,是前端工程师绑定不了的基本功。

二、CSR 工作原理

CSR 即客户端渲染,页面的 HTML 结构和内容完全由浏览器端的 JavaScript 生成。

加载流程

  1. 浏览器请求页面,服务器返回一个几乎为空的 HTML 文件(只有一个挂载点和 <script> 标签)
  2. 浏览器下载并解析 JavaScript Bundle
  3. JS 执行,框架初始化,发起数据请求(Ajax / Fetch)
  4. 拿到数据后,框架渲染 DOM,页面内容呈现
用户请求 → 空 HTML → 下载 JS → 执行 JS → 请求数据 → 渲染页面
                 ↑                                    ↑
              白屏开始                              白屏结束

白屏问题的根源

白屏时间 = HTML 下载 + JS 下载 + JS 解析执行 + 数据请求 + 渲染。这条链路上的每一环都在延长用户等待时间。Bundle 越大、网络越差、设备性能越低,白屏越严重。

CSR 的典型代码结构(以 React 为例):

// main.tsx
import { createRoot } from 'react-dom/client';
import App from './App';

const root = createRoot(document.getElementById('root')!);
root.render(<App />);
// App.tsx
import { useEffect, useState } from 'react';

function App() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetch('/api/posts')
      .then(response => response.json())
      .then(data => setPosts(data));
  }, []);

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.summary}</p>
        </article>
      ))}
    </div>
  );
}

页面内容依赖 useEffect 中的异步请求,搜索引擎爬虫在抓取时拿到的只是空 HTML,无法索引实际内容。

三、SSR 工作原理

SSR 即服务端渲染,服务器在接收到请求时,执行组件的渲染逻辑,将完整的 HTML 字符串返回给浏览器。

加载流程

  1. 浏览器请求页面
  2. 服务器执行组件渲染,调用数据接口,生成包含完整内容的 HTML
  3. 浏览器收到 HTML,直接解析渲染,用户立即看到页面内容
  4. 浏览器下载并执行 JS,框架接管页面,绑定事件监听器(Hydration)
用户请求 → 服务器渲染 HTML → 浏览器解析 HTML → 用户看到内容

                           下载 JS → Hydration → 页面可交互

以 Next.js 的 Pages Router 为例:

// pages/posts.tsx
import type { GetServerSideProps } from 'next';

interface Post {
  id: number;
  title: string;
  summary: string;
}

export const getServerSideProps: GetServerSideProps = async () => {
  const response = await fetch('https://api.example.com/posts');
  const posts: Post[] = await response.json();

  return { props: { posts } };
};

export default function PostsPage({ posts }: { posts: Post[] }) {
  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.summary}</p>
        </article>
      ))}
    </div>
  );
}

getServerSideProps 在服务端执行,数据获取和 HTML 生成都在服务器完成,浏览器拿到的是带有完整内容的 HTML。

Hydration(注水)

服务端返回的 HTML 是"死的"——只有 DOM 结构和文本,没有事件绑定。Hydration 的作用是让客户端 JS 接管服务端生成的 DOM,为其附加事件监听器和状态管理,使页面变得可交互

Hydration 的过程并不是重新渲染:框架会复用服务端生成的 DOM 节点,只进行事件绑定和状态同步。如果客户端渲染结果与服务端 HTML 不一致,会触发 Hydration Mismatch 警告,严重时可能导致页面闪烁或功能异常。

常见导致 Hydration Mismatch 的原因:

  • 使用了 Date.now()Math.random() 等非确定性 API
  • 根据 windowlocalStorage 的值条件渲染,但服务端没有这些对象
  • 服务端和客户端的时区、语言环境不一致

四、SSR vs CSR 对比

维度CSRSSR
首屏性能差,需等 JS 下载执行后才显示内容好,服务端直出 HTML,用户快速看到内容
SEO差,爬虫抓取到空 HTML好,爬虫直接获取完整内容
交互体验首屏后页面切换流畅,无需刷新首屏快但 Hydration 前不可交互,存在 TTI 延迟
服务器压力低,服务器只提供静态文件和 API高,每次请求都需要服务端执行渲染
开发复杂度低,纯前端开发高,需处理服务端/客户端代码兼容性
缓存策略前端资源 CDN 缓存即可需要额外的页面级缓存策略(如 Redis 缓存 HTML)
FCP
TTIFCP ≈ TTI,内容出现即可交互FCP 快但 TTI 滞后,存在"可见不可用"阶段

FCP(First Contentful Paint):首次内容绘制,用户第一次看到页面内容的时间。 TTI(Time to Interactive):可交互时间,页面完全可响应用户操作的时间。

SSR 存在一个容易被忽略的问题:页面看起来已经加载完了,但按钮点不了、表单填不了。这段"可见不可用"的时间差,在 JS Bundle 较大或网络较慢的情况下尤为明显,对用户体验的伤害有时比白屏更大。

五、同构渲染

同构渲染(Isomorphic / Universal Rendering)是 SSR 和 CSR 的结合:同一套组件代码既在服务端运行生成 HTML,又在客户端运行实现交互

核心思路

  1. 服务端执行组件渲染,输出完整 HTML,解决首屏性能和 SEO 问题
  2. 客户端下载 JS 后执行 Hydration,接管页面,实现 SPA 级别的交互体验
  3. 后续页面跳转走客户端路由,不再请求服务端渲染

这样既拿到了 SSR 的首屏优势,又保留了 CSR 页面切换无刷新的流畅体验。

Next.js 实现思路

Next.js 是 React 生态中最成熟的同构渲染框架。它的核心机制:

  • Pages Router:通过 getServerSideProps(SSR)、getStaticProps(SSG)决定数据获取时机
  • App Router(v13+):默认所有组件为 Server Component,需要交互的组件用 'use client' 标记为 Client Component
// App Router 示例 - Server Component(默认)
// app/posts/page.tsx
async function PostsPage() {
  const posts = await fetch('https://api.example.com/posts').then(
    response => response.json()
  );

  return (
    <div>
      {posts.map((post: { id: number; title: string; summary: string }) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.summary}</p>
          <LikeButton postId={post.id} />
        </article>
      ))}
    </div>
  );
}
// Client Component - 需要交互的部分
// app/posts/LikeButton.tsx
'use client';

import { useState } from 'react';

export function LikeButton({ postId }: { postId: number }) {
  const [liked, setLiked] = useState(false);

  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? '❤️' : '🤍'}
    </button>
  );
}

Server Component 的渲染在服务端完成,不会打进客户端 Bundle;Client Component 才会被发送到浏览器执行。这种拆分大幅减小了客户端 JS 体积。

Nuxt.js 实现思路

Nuxt.js 是 Vue 生态的同构方案,思路类似:

  • 默认开启 SSR,页面组件在服务端渲染
  • 通过 useAsyncData / useFetch 在服务端获取数据
  • 客户端 Hydration 后切换为 SPA 模式
<!-- pages/posts.vue -->
<script setup>
const { data: posts } = await useFetch('/api/posts');
</script>

<template>
  <div>
    <article v-for="post in posts" :key="post.id">
      <h2>{{ post.title }}</h2>
      <p>{{ post.summary }}</p>
    </article>
  </div>
</template>

同构开发的注意事项

同构代码需要在服务端和客户端都能运行,这引入了额外的约束:

  • 不能在组件顶层访问浏览器 APIwindowdocumentlocalStorage),需要在 useEffectonMounted 等客户端生命周期中使用
  • 服务端没有用户交互,事件处理函数不会在服务端执行
  • 第三方库兼容性:部分库依赖浏览器环境,需要动态导入或条件加载
  • 内存泄漏风险:服务端渲染是多用户共享的,全局变量和单例模式可能导致数据串联

六、SSG 静态站点生成

SSG(Static Site Generation)在构建时生成所有页面的静态 HTML 文件,部署后直接通过 CDN 分发,无需服务器运行时渲染。

// Next.js Pages Router - SSG
export const getStaticProps: GetStaticProps = async () => {
  const posts = await fetch('https://api.example.com/posts').then(
    response => response.json()
  );
  return {
    props: { posts },
  };
};

// 动态路由需要配合 getStaticPaths
export const getStaticPaths: GetStaticPaths = async () => {
  const posts = await fetch('https://api.example.com/posts').then(
    response => response.json()
  );
  const paths = posts.map((post: { slug: string }) => ({
    params: { slug: post.slug },
  }));
  return { paths, fallback: false };
};

SSG 的优势

  • 性能极致:纯静态文件,CDN 缓存,响应速度最快
  • 服务器成本几乎为零
  • 安全性高:没有服务端运行时,攻击面极小

SSG 的局限

  • 内容更新需要重新构建部署
  • 页面数量极大时(数十万页面),构建时间可能无法接受
  • 无法展示实时个性化内容

适用场景:博客、文档站、营销落地页、产品介绍页等内容相对固定的站点。

七、ISR 增量静态再生

ISR(Incremental Static Regeneration)是 Next.js 提出的方案,结合了 SSG 的性能和 SSR 的数据时效性:页面在构建时静态生成,但可以按设定的时间间隔在后台重新生成

// Next.js ISR
export const getStaticProps: GetStaticProps = async () => {
  const posts = await fetch('https://api.example.com/posts').then(
    response => response.json()
  );
  return {
    props: { posts },
    revalidate: 60, // 60 秒后,下一次请求触发后台重新生成
  };
};

ISR 的工作机制:

  1. 构建时生成静态页面
  2. 用户请求到来时,如果页面未过期(未超过 revalidate 时间),直接返回缓存的静态页面
  3. 如果已过期,仍然先返回旧页面(用户不会感知延迟),同时在后台触发页面重新生成
  4. 下一次请求将获得新生成的页面

这种"stale-while-revalidate"策略确保了用户始终能快速获得页面,同时数据也能在可接受的延迟内更新。

ISR 适用场景:电商商品详情页、新闻资讯列表等需要一定数据时效性但不要求实时的页面。

八、流式 SSR(React 18 Streaming)

传统 SSR 需要等整个页面渲染完成后才能发送 HTML 给浏览器。如果页面中有慢查询接口,整个页面的 TTFB(Time to First Byte)都会被拖慢。

React 18 引入了 Streaming SSR,核心思想是:将页面拆分为多个块,渲染完一部分就发送一部分,不必等全部完成

Suspense + Streaming

// app/dashboard/page.tsx
import { Suspense } from 'react';

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      {/* 头部信息立即返回 */}
      <UserInfo />

      {/* 数据报表需要慢查询,用 Suspense 包裹 */}
      <Suspense fallback={<div>加载报表中...</div>}>
        <AnalyticsChart />
      </Suspense>

      {/* 推荐列表也需要异步数据 */}
      <Suspense fallback={<div>加载推荐...</div>}>
        <RecommendationList />
      </Suspense>
    </div>
  );
}

流式 SSR 的工作过程:

  1. 服务端开始渲染,先把 <h1>Dashboard</h1><UserInfo /> 发送给浏览器
  2. AnalyticsChart 的数据还没准备好,先发送 fallback 占位内容
  3. 浏览器已经可以开始渲染已收到的部分
  4. AnalyticsChart 数据就绪,服务端再推送实际内容替换占位
  5. RecommendationList 同理

关键收益

  • TTFB 大幅缩短,用户更早看到页面框架
  • 慢接口不会阻塞整个页面
  • 配合 Selective Hydration,先到达的部分可以优先变得可交互

与传统 SSR 的对比

维度传统 SSR流式 SSR
HTML 发送时机全部渲染完毕后一次性发送渲染完一部分发送一部分
TTFB受最慢接口影响快,无需等待全部数据
Hydration整页一次性 HydrationSelective Hydration,逐块激活
用户体验要么白屏,要么完整页面渐进式加载,优先展示关键内容

九、技术选型:何时用什么

没有银弹,选型取决于业务场景。

用 CSR 的场景

  • 后台管理系统:不需要 SEO,用户已登录,交互密集
  • 需要复杂实时交互的应用:在线编辑器、实时协作工具
  • 纯内部工具:不面向公众,对首屏性能要求不高

用 SSR 的场景

  • 强 SEO 需求 + 频繁更新的内容:社交平台、论坛、UGC 内容站
  • 首屏性能要求高:电商首页、新闻门户
  • 需要根据用户身份个性化渲染:不同用户看到不同内容的页面

用 SSG 的场景

  • 内容更新频率低:博客、文档、帮助中心
  • 极致性能需求:营销落地页、产品官网
  • 成本敏感:不想维护服务器,纯 CDN 托管

用 ISR 的场景

  • 内容有一定时效性但非实时:商品详情页、新闻文章
  • 页面数量大:无法接受全量 SSG 的构建时间,但又想要静态性能

混合使用

现代框架支持在同一个项目中混合使用不同渲染策略。Next.js App Router 可以在路由级别选择渲染模式:

// 静态页面 - SSG(默认行为)
// app/about/page.tsx
export default function AboutPage() {
  return <div>关于我们</div>;
}

// 动态页面 - SSR
// app/profile/page.tsx
export const dynamic = 'force-dynamic';

export default async function ProfilePage() {
  const user = await getCurrentUser();
  return <div>欢迎, {user.name}</div>;
}

// ISR
// app/products/[id]/page.tsx
export const revalidate = 3600; // 1 小时

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);
  return <div>{product.name}</div>;
}

十、面试高频追问

Q:SSR 一定比 CSR 快吗?

不一定。SSR 的首屏展示速度通常更快(FCP 更早),但 TTI 可能反而更慢。如果服务端接口响应慢,SSR 的 TTFB 也会很长。此外,SSR 增加了服务器计算开销,在高并发下如果没有合理的缓存策略,服务端可能成为性能瓶颈。

Q:Hydration 失败会怎样?

React 在检测到 Hydration Mismatch 时,开发模式下会在控制台打印警告。严重的不匹配会导致 React 放弃复用服务端 DOM,退化为完整的客户端重新渲染,引起页面闪烁。在 React 18 中,严重的 Hydration 错误会直接导致整个应用回退到客户端渲染。

Q:SSR 如何应对高并发?

几种常见策略:

  • 页面级缓存:将渲染好的 HTML 缓存到 Redis 或 CDN 边缘节点,相同请求直接返回缓存
  • 降级方案:服务端渲染超时或出错时,降级返回 CSR 版本的 HTML
  • ISR / SSG 优先:将不需要实时数据的页面转为静态生成,减少服务端渲染压力
  • 流式渲染:使用 Streaming SSR 缩短 TTFB,降低单次渲染的内存占用

Q:React Server Component 和 SSR 的区别?

SSR 是一种渲染时机——在服务端生成 HTML。Server Component 是一种组件类型——只在服务端执行,不会发送 JS 到客户端。SSR 生成 HTML 后仍然需要完整的客户端 JS 进行 Hydration;而 Server Component 的代码压根不会出现在客户端 Bundle 中。两者可以配合使用:Server Component 在服务端渲染,通过 Streaming SSR 流式发送给客户端。

Q:SSG 页面如何实现动态内容?

SSG 页面本身是静态的,但可以通过以下方式加入动态部分:

  • 客户端 fetch:页面加载后通过 API 请求动态数据
  • ISR:设置 revalidate 定时更新静态页面
  • 边缘函数:在 CDN 边缘节点注入个性化内容(如 Vercel Edge Middleware)

十一、总结

  • CSR 适合交互密集、不需要 SEO 的场景,开发简单但首屏性能差
  • SSR 解决了首屏性能和 SEO 问题,但增加了服务器压力和开发复杂度
  • 同构渲染 结合两者优势,是现代 Web 应用的主流方案
  • SSG 适合内容静态的站点,性能最优、成本最低
  • ISR 在 SSG 基础上提供了数据更新能力,适合内容有一定时效性的场景
  • 流式 SSR 通过分块发送和 Selective Hydration 进一步优化了传统 SSR 的体验

技术选型不是非此即彼,现代框架已经支持在同一项目中按路由粒度混合使用不同渲染策略。核心判断依据是:这个页面的内容是否需要 SEO、数据更新频率如何、对首屏性能的要求多高、是否有服务端基础设施支撑