名词(术语)了解–SSR/CSR
什么是服务器端渲染(SSR)?
服务器端渲染是指由服务器生成完整的 HTML 页面,然后发送给客户端的过程。
这与客户端渲染(CSR)形成对比,后者主要依赖浏览器端的 JavaScript 来渲染页面内容。
详情
客户端发送页面请求
- 用户访问网站 URL
- 浏览器向服务器发送 HTTP 请求
服务器请求数据
// 服务器端代码 async function fetchData() { const data = await db.query('SELECT * FROM posts'); return data; }
数据库返回数据
// 数据示例 const data = { posts: [ { id: 1, title: '文章1' }, { id: 2, title: '文章2' } ] };
服务器执行组件代码
// React 组件 function App({ data }) { return ( <div> {data.posts.map(post => ( <article key={post.id}> <h2>{post.title}</h2> </article> ))} </div> ); }
生成 HTML
// 服务器端渲染 const html = ReactDOMServer.renderToString( <App data={data} /> );
返回完整 HTML
<!DOCTYPE html> <html> <head> <title>SSR App</title> </head> <body> <div id="root">${html}</div> <script> window.__INITIAL_STATE__ = ${JSON.stringify(data)} </script> <script src="/client.js"></script> </body> </html>
客户端显示内容
- 浏览器接收到完整的 HTML
- 用户立即看到页面内容
- 无需等待 JavaScript 加载
加载 JavaScript
// 客户端 JavaScript const initialState = window.__INITIAL_STATE__;
Hydration 过程
// 客户端 hydration ReactDOM.hydrate( <App data={initialState} />, document.getElementById('root') );
SSR 的主要优点:
更快的首屏加载
- 用户立即看到完整内容
- 无需等待 JavaScript 下载和执行
更好的 SEO
- 搜索引擎可以直接爬取完整的 HTML 内容
- 有利于网站的搜索引擎排名
更好的性能
- 减少客户端计算负担
- 适合低性能设备
更好的用户体验
- 无空白页面等待
- 更快的内容可见时间
SSR 的挑战:
服务器负载
- 需要更多服务器资源
- 需要合理的缓存策略
开发复杂性
- 需要同构代码(服务端和客户端都能运行)
- 需要处理服务端特有的问题
部署要求
- 需要 Node.js 环境
- 需要更复杂的部署配置
适用场景:
- 内容展示型网站
- 需要 SEO 的网站
- 首屏加载速度要求高的应用
- 面向低端设备用户的应用
框架支持:
- Next.js (React)
- Nuxt.js (Vue)
- SvelteKit (Svelte)
- Remix (React)
什么是客户端渲染(CSR)?
CSR(客户端渲染)的工作流程说明:
-
初始请求:
- 用户在浏览器中输入URL或点击链接
- 浏览器向服务器发送页面请求
-
服务器响应:
- 服务器返回一个基本的空HTML文件
- HTML文件中包含必要的JS包引用(React应用代码)
-
加载过程:
- 浏览器下载JS包
- 这个阶段用户看到的是空白页面或加载指示器
-
应用初始化:
- JS包加载完成后,React应用开始初始化
- 创建虚拟DOM和应用状态
-
组件挂载:
- React组件树开始挂载
- 初始化组件生命周期
-
数据获取:
- 组件挂载后发起API请求
- 向后端服务器请求需要的数据
-
数据响应:
- API服务器返回请求的数据
- 数据以JSON格式传输
-
状态更新:
- React组件接收到数据
- 更新组件状态(setState)
-
内容渲染:
- React根据新状态重新渲染组件
- 更新实际DOM
-
完成显示:
- 用户最终看到完整的页面内容
- 页面可以交互
详情:
-
浏览器请求页面
- 用户访问网站 URL
- 浏览器向服务器发送 HTTP 请求
-
服务器响应
- 返回基础 HTML 文件(通常只包含一个 root div)
- 返回打包后的 JS 文件(包含 React 应用代码)
<!DOCTYPE html> <html> <head> <title>React App</title> </head> <body> <div id="root"></div> <script src="/static/js/bundle.js"></script> </body> </html>
-
JS 包加载
- 浏览器下载 JavaScript 文件
- 解析并执行 JavaScript 代码
- 此时用户看到空白页面
-
React 初始化
ReactDOM.createRoot( document.getElementById('root') ).render(<App />);
-
组件挂载
function App() { useEffect(() => { // 组件挂载后执行 }, []); return <div>Loading...</div>; }
-
数据请求
const [data, setData] = useState(null); useEffect(() => { fetch('/api/data') .then(res => res.json()) .then(data => setData(data)); }, []);
-
接收数据
{ "status": "success", "data": { "items": [...] } }
-
更新状态
setData(receivedData);
-
渲染内容
return ( <div> {data ? ( <DataDisplay data={data} /> ) : ( <Loading /> )} </div> );
-
页面可交互
- 用户可以看到完整内容
- 可以进行点击、输入等交互操作
CSR 的关键特点:
- 首次加载时需要下载完整的 JavaScript 包
- 所有页面路由和视图转换都在客户端处理
- 数据获取和页面更新都是异步进行的
- 适合构建单页应用(SPA)
- 需要考虑首屏加载优化:
- 代码分割(Code Splitting)
- 懒加载(Lazy Loading)
- 预加载(Preloading)
- 合理的缓存策略
CSR的优缺点:
优点:
- 前后端完全分离
- 用户体验好,切换页面快
- 减轻服务器压力
- 客户端缓存友好
缺点:
- 首屏加载较慢
- SEO不友好
- 对JavaScript依赖性强
- 在低性能设备上体验欠佳
适用场景:
- 后台管理系统
- 交互密集型应用
- 实时数据展示
- 不需要SEO的应用
不适用场景:
- 需要良好SEO的网站
- 首屏加载速度要求极高的页面
- 低端设备用户较多的应用
SSR的主要特点
-
渲染过程
- 在服务器端完成页面的渲染
- 生成完整的HTML文档
- 客户端接收到的是完整的页面内容
-
性能特征
- 更快的首屏加载时间
- 更好的SEO表现
- 较高的服务器资源消耗
-
适用场景
- 内容密集型网站
- 需要良好SEO的网站
- 首屏加载速度要求高的应用
常见SSR框架
-
Next.js
- React生态系统中最流行的SSR框架
- 提供了自动静态优化
- 支持增量静态生成(ISR)
-
Nuxt.js
- Vue.js的SSR框架
- 提供自动路由配置
- 支持静态站点生成
-
Angular Universal
- Angular的SSR解决方案
- 支持预渲染
- 提供服务器端API
使用建议
-
何时使用SSR
- 网站需要优秀的SEO表现
- 用户期望快速的首屏加载
- 网站内容频繁更新
- 网站有大量动态内容
-
何时避免使用SSR
- 应用交互性很强
- 服务器资源有限
- 内容更新频率低
- 主要面向已登录用户
-
性能优化建议
- 实施缓存策略
- 使用CDN
- 合理配置服务器资源
- 优化数据获取逻辑
SSR场景示例
1. 博客文章页面 (Next.js)
这是一个典型的内容展示场景,非常适合使用SSR:
id: blog-ssr
name: Blog Post with SSR
type: tsx
content: |-
// pages/posts/[id].tsx
import React from 'react'
import { GetServerSideProps } from 'next'
interface Post {
id: number
title: string
content: string
author: string
publishDate: string
}
interface BlogPostProps {
post: Post
}
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
// 在服务器端获取文章数据
const res = await fetch(`https://api.example.com/posts/${params?.id}`)
const post = await res.json()
return {
props: {
post
}
}
}
const BlogPost: React.FC<BlogPostProps> = ({ post }) => {
return (
<div className="max-w-3xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-4">{post.title}</h1>
<div className="text-gray-600 mb-4">
<span>作者:{post.author}</span>
<span className="ml-4">发布时间:{post.publishDate}</span>
</div>
<div className="prose">
{post.content}
</div>
</div>
)
}
export default BlogPost
2. 电商产品列表 (Next.js)
电商场景需要良好的SEO和快速的首屏加载:
id: ecommerce-ssr
name: E-commerce Product List
type: tsx
content: |-
// pages/products.tsx
import React from 'react'
import { GetServerSideProps } from 'next'
import { Select } from "@/components/ui/select"
interface Product {
id: number
name: string
price: number
description: string
image: string
}
interface ProductsPageProps {
products: Product[]
categories: string[]
}
export const getServerSideProps: GetServerSideProps = async ({ query }) => {
// 获取分类和筛选条件
const category = query.category || 'all'
const sort = query.sort || 'default'
// 在服务器端获取商品数据
const productsRes = await fetch(
`https://api.example.com/products?category=${category}&sort=${sort}`
)
const products = await productsRes.json()
// 获取分类列表
const categoriesRes = await fetch('https://api.example.com/categories')
const categories = await categoriesRes.json()
return {
props: {
products,
categories
}
}
}
const ProductsPage: React.FC<ProductsPageProps> = ({ products, categories }) => {
return (
<div className="container mx-auto p-6">
<div className="flex justify-between mb-6">
<h1 className="text-2xl font-bold">商品列表</h1>
<Select className="w-48">
{categories.map(category => (
<option key={category} value={category}>
{category}
</option>
))}
</Select>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{products.map(product => (
<div key={product.id} className="border rounded-lg p-4">
<img
src={product.image}
alt={product.name}
className="w-full h-48 object-cover mb-4"
/>
<h2 className="text-xl font-semibold">{product.name}</h2>
<p className="text-gray-600">{product.description}</p>
<div className="mt-4 flex justify-between items-center">
<span className="text-xl text-red-600">¥{product.price}</span>
<button className="bg-blue-500 text-white px-4 py-2 rounded">
加入购物车
</button>
</div>
</div>
))}
</div>
</div>
)
}
export default ProductsPage
3. 新闻门户首页 (Nuxt.js)
新闻网站需要实时性和SEO,很适合SSR:
id: news-ssr
name: News Portal
type: tsx
content: |-
// pages/index.vue
<template>
<div class="container mx-auto p-6">
<header class="mb-8">
<h1 class="text-4xl font-bold mb-4">今日头条</h1>
<div class="flex gap-4">
<button
v-for="category in categories"
:key="category"
class="px-4 py-2 rounded-full"
:class="selectedCategory === category ? 'bg-blue-500 text-white' : 'bg-gray-200'"
@click="selectCategory(category)"
>
{{ category }}
</button>
</div>
</header>
<main>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<article
v-for="news in newsList"
:key="news.id"
class="border rounded-lg overflow-hidden"
>
<img :src="news.image" :alt="news.title" class="w-full h-48 object-cover">
<div class="p-4">
<h2 class="text-xl font-bold mb-2">{{ news.title }}</h2>
<p class="text-gray-600 mb-4">{{ news.summary }}</p>
<div class="flex justify-between items-center text-sm text-gray-500">
<span>{{ news.source }}</span>
<span>{{ news.publishTime }}</span>
</div>
</div>
</article>
</div>
</main>
</div>
</template>
<script>
export default {
async asyncData({ $axios }) {
const [categories, newsList] = await Promise.all([
$axios.$get('https://api.example.com/categories'),
$axios.$get('https://api.example.com/news')
])
return {
categories,
newsList,
selectedCategory: 'all'
}
},
methods: {
async selectCategory(category) {
this.selectedCategory = category
this.newsList = await this.$axios.$get(
`https://api.example.com/news?category=${category}`
)
}
},
head() {
return {
title: '新闻门户 - 今日头条',
meta: [
{
hid: 'description',
name: 'description',
content: '最新、最热门的新闻资讯'
}
]
}
}
}
</script>
4. 仪表盘页面 (混合渲染)
对于仪表盘这类应用,我们可以使用混合渲染策略:
id: dashboard-ssr
name: Dashboard with Hybrid Rendering
type: tsx
content: |-
// pages/dashboard.tsx
import React, { useEffect, useState } from 'react'
import { GetServerSideProps } from 'next'
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts'
interface DashboardData {
summary: {
totalUsers: number
activeUsers: number
revenue: number
}
historicalData: {
date: string
users: number
revenue: number
}[]
}
interface DashboardProps {
initialData: DashboardData
}
export const getServerSideProps: GetServerSideProps = async () => {
// 获取初始数据
const res = await fetch('https://api.example.com/dashboard/initial')
const initialData = await res.json()
return {
props: {
initialData
}
}
}
const Dashboard: React.FC<DashboardProps> = ({ initialData }) => {
const [realtimeData, setRealtimeData] = useState(null)
useEffect(() => {
// 客户端连接WebSocket获取实时数据
const ws = new WebSocket('wss://api.example.com/dashboard/realtime')
ws.onmessage = (event) => {
setRealtimeData(JSON.parse(event.data))
}
return () => ws.close()
}, [])
return (
<div className="container mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">运营数据仪表盘</h1>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-2">总用户数</h3>
<p className="text-3xl">
{realtimeData?.totalUsers || initialData.summary.totalUsers}
</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-2">活跃用户</h3>
<p className="text-3xl">
{realtimeData?.activeUsers || initialData.summary.activeUsers}
</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-2">营收(元)</h3>
<p className="text-3xl">
{realtimeData?.revenue || initialData.summary.revenue}
</p>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">历史趋势</h3>
<LineChart
width={800}
height={400}
data={initialData.historicalData}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="users" stroke="#8884d8" />
<Line type="monotone" dataKey="revenue" stroke="#82ca9d" />
</LineChart>
</div>
</div>
)
}
export default Dashboard
各场景特点说明
-
博客文章页面
- 使用
getServerSideProps
在服务器端获取文章数据 - 完全的SSR渲染,有利于SEO
- 适合内容不经常变化的场景
- 使用
-
电商产品列表
- 支持动态分类和筛选
- 服务器端预渲染商品数据
- 结合客户端交互(分类选择、加入购物车)
-
新闻门户首页
- 使用Nuxt.js的
asyncData
进行服务器端数据获取 - 支持动态切换分类
- 针对SEO优化的meta信息
- 使用Nuxt.js的
-
仪表盘页面
- 混合渲染策略:
- 初始数据通过SSR加载
- 实时数据通过客户端WebSocket更新
- 使用Recharts进行数据可视化
- 响应式布局设计
- 混合渲染策略:
最佳实践建议
-
数据获取
- 在服务器端获取关键数据
- 使用适当的缓存策略
- 考虑数据的实时性需求
-
性能优化
- 实现增量静态再生成(ISR)
- 使用适当的缓存策略
- 优化图片和资源加载
-
用户体验
- 实现平滑的客户端交互
- 添加适当的加载状态
- 处理错误情况
-
SEO优化
- 添加适当的meta标签
- 实现结构化数据
- 确保内容的可访问性
结论
SSR是一种强大的渲染方式,特别适合需要良好SEO和快速首屏加载的应用。