2025年05月28日6 分钟
CSR 客户端渲染:原理、流程与适用场景
深入理解 CSR(Client-Side Rendering)的工作原理,分析其优缺点和适用场景
什么是 CSR
CSR(Client-Side Rendering,客户端渲染)是指页面的 HTML 内容完全由浏览器端的 JavaScript 生成。服务器只返回一个几乎为空的 HTML 外壳和一个 JS 文件,浏览器下载并执行 JS 后,才动态生成页面内容。
React(Create React App)、Vue CLI 的默认模式就是 CSR。
CSR 的完整流程
用户请求 https://example.com
│
▼
┌─────────────────┐
│ 服务器返回 │ 极简 HTML(几乎是空的)
│ 空 HTML │
└────────┬────────┘
│
▼ 浏览器收到 HTML
┌─────────────────────────────────────────┐
│ <!DOCTYPE html> │
│ <html> │
│ <body> │
│ <div id="root"></div> ← 空白! │
│ <script src="/bundle.js"></script> │
│ </body> │
│ </html> │
└────────┬────────────────────────────────┘
│
▼ 用户此时看到白屏 ⬜
│
│ 下载 bundle.js(可能 200KB~2MB)
│
▼ JS 下载完成
│
│ 解析 + 执行 JavaScript
│ React 初始化
│ 组件树渲染
│ 调用 API 获取数据
│
▼ API 返回数据
│
│ React 用数据渲染真实 DOM
│ 插入 #root 中
│
▼ 用户终于看到完整页面 ✅
时间线
0ms 200ms 800ms 1500ms 2500ms
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
请求发出 HTML到达 JS下载完 JS执行完 API数据到达
白屏开始 开始解析 首次渲染 完整渲染
(FP) (开始执行) (FCP) (LCP)
├───── 白屏时间 ────────┤
├── 内容加载 ──┤
CSR 的代码实现
典型的入口文件
// index.html — 服务器返回的空壳
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="root"></div>
<script src="/static/js/bundle.js"></script>
</body>
</html>
// main.tsx — JS 入口
import { createRoot } from 'react-dom/client'
import App from './App'
const root = createRoot(document.getElementById('root')!)
root.render(<App />)
// App.tsx — 应用组件
function App() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data)
setLoading(false)
})
}, [])
if (loading) return <Spinner />
return <UserList users={users} />
}
路由也在客户端处理
// React Router — 所有路由都由 JS 处理
import { BrowserRouter, Routes, Route } from 'react-router-dom'
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users/:id" element={<UserDetail />} />
</Routes>
</BrowserRouter>
)
}
// 切换路由时不会向服务器发请求
// JS 拦截 URL 变化,直接渲染对应组件
CSR 的优缺点
优点
| 优点 | 说明 |
|---|---|
| 交互体验流畅 | 首次加载后,页面切换几乎是即时的(SPA) |
| 服务器压力小 | 服务器只需要提供静态文件和 API |
| 前后端分离 | 前端独立部署,后端只提供 API |
| 部署简单 | 静态文件可以放在 CDN 上 |
| 富交互支持 | 适合复杂的用户交互(拖拽、实时编辑、动画) |
缺点
| 缺点 | 说明 |
|---|---|
| 白屏时间长 | 用户要等 JS 下载+执行后才能看到内容 |
| SEO 不友好 | 搜索引擎爬虫可能看不到内容(只看到空 div) |
| 首屏性能差 | FCP/LCP 指标较差 |
| JS 依赖重 | JS 加载失败 = 页面完全不可用 |
SEO 问题详解
Google 爬虫请求页面:
CSR 返回的 HTML:
<div id="root"></div>
爬虫看到的内容:空的!
← 虽然 Google 能执行 JS,但有延迟和不确定性
← 其他搜索引擎(百度)可能完全不执行 JS
适用场景
✅ 适合 CSR 的场景:
├── 后台管理系统(不需要 SEO)
├── SaaS 工具(Figma、Notion、飞书)
├── 内部系统(CRM、ERP)
├── 需要复杂交互的 Web 应用
└── 实时数据看板
❌ 不适合 CSR 的场景:
├── 内容型网站(博客、新闻、文档)
├── 电商产品页(需要 SEO)
├── 营销落地页(首屏速度关键)
└── 对首次加载性能要求高的场景
CSR 的优化方案
1. 代码分割(Code Splitting)
import { lazy, Suspense } from 'react'
// 路由级别代码分割
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
)
}
// 用户只下载当前路由需要的代码,不是整个 bundle
2. 骨架屏(Skeleton)
// 减少用户感知的白屏时间
function UserList() {
const { data, isLoading } = useFetch('/api/users')
if (isLoading) return <UserListSkeleton /> // 灰色占位块
return <RealUserList data={data} />
}
3. 预渲染(Prerendering)
# 构建时为每个路由生成静态 HTML
# react-snap / prerender-spa-plugin
npx react-snap
# 输出:每个路由的完整 HTML(含内容)
# 爬虫能看到内容,用户首屏也更快
总结
CSR 是最简单的渲染模式,适合不需要 SEO、交互复杂的 Web 应用。对于内容型网站,应该考虑 SSR 或 SSG 来解决白屏和 SEO 问题。