名词(术语)了解–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].tsximport React from 'react'import { GetServerSideProps } from 'next'interface Post {id: numbertitle: stringcontent: stringauthor: stringpublishDate: 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.tsximport React from 'react'import { GetServerSideProps } from 'next'import { Select } from "@/components/ui/select"interface Product {id: numbername: stringprice: numberdescription: stringimage: 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 = categorythis.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.tsximport React, { useEffect, useState } from 'react'import { GetServerSideProps } from 'next'import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts'interface DashboardData {summary: {totalUsers: numberactiveUsers: numberrevenue: number}historicalData: {date: stringusers: numberrevenue: 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><LineChartwidth={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和快速首屏加载的应用。