何时使用服务端渲染(SSR)?

引言
现代前端工具为我们提供了如此多的选择:SSR、CSR、SSG、ISR、边缘函数等,很容易被潮流所吸引。我见过团队仅仅因为SSR听起来"企业级"就跳上SSR的船,或者在没有考虑SEO或性能影响的情况下就完全采用SPA。
事实是:每种渲染策略都是一种权衡。实际上,这是软件架构的第一定律:"一切都是权衡"
SSR本身并不更好或更差;重要的是你在哪里使用它以及为什么使用它。
SSR可以很好地解决特定问题,特别是在SEO、性能、安全性和网络优化方面。但是,它也带来了运营开销(想想服务器冷启动、缓存策略、延迟调优等)。所以,在采用它之前,要理解你要解决什么问题。
在这篇文章中,我将介绍SSR有帮助的用例,以及CSR会失败或不必要地使事情复杂化的地方。
使用场景
1. SEO索引和网络爬虫
如果你的应用或网站需要被搜索引擎发现(如博客文章、产品页面和落地页),SSR是必经之路。谷歌在渲染JavaScript方面确实有所改进,但依赖这一点是有风险的。使用SSR,搜索引擎可以立即获得完全形成的HTML——无需额外工作。
把它想象成给谷歌一个干净的、现成的盘子,而不是给它配料并要求它烹饪。
2. 保持秘密令牌的秘密性
有时,你需要使用秘密密钥(API令牌、数据库凭据等)来获取数据。你不希望这些秘密泄露到客户端包中。
SSR在服务器上运行,这意味着它可以安全地访问这些秘密,而不会将它们暴露给浏览器。
这在集成私有API或服务器到服务器通信时特别有用。
3. 减少用户的网络调用
另一个重大胜利:当你能在服务器上做繁重的工作时。
假设你需要进行多个API调用或合并结果;在服务器上这样做可以减少客户端的网络和计算负担。
用户获得一个准备渲染的页面,往返次数更少,TTFP更快,在慢速设备或网络上的性能更好。
4. 无需客户端往返的个性化
如果你需要根据用户数据(如位置、认证会话、设备类型)个性化内容,SSR允许你在第一次加载时直接将此注入HTML中。没有闪烁的空状态,没有等待客户端JavaScript获取用户信息并重新渲染DOM。
想想基于地理位置的优惠、特定地区的定价或用户仪表板预览——所有这些都从服务器渲染,具有上下文感知的HTML。
这避免了布局偏移,减少了感知加载时间,并确保用户不会在页面适应之前看到"默认"页面。
5. 更快的TTFP(首次绘制时间)
SSR为你提供了发送准备绘制的HTML的优势,这可以显著减少TTFP和首次内容绘制(FCP),特别是在慢速网络或预算设备上。
CSR需要下载JS、解析、执行,然后渲染内容。
SSR跳过所有这些,给浏览器提供准备绘制的标记。
当然,你仍然需要优化服务器延迟(例如,缓存、预取),但用户能更快地在屏幕上看到内容。
6. 实时或频繁更新的内容
如果你正在构建数据经常变化的页面(例如,新闻标题、股票行情、仪表板),SSR可以在服务器端获取最新数据并交付最新视图,而不依赖客户端在加载后获取和重新水合。
你直接从服务器获得新鲜内容,没有API陈旧或"加载闪烁疲劳"
SSR的权衡(这不是免费的午餐)
像任何架构选择一样,SSR也有自己的权衡。在有意义的地方使用它,但要意识到你正在签署什么。
🐌 服务器延迟
SSR依赖于服务器响应时间。如果你的后端很慢,整个页面就会被阻塞。你需要优化API调用,有时并行化数据获取以避免瓶颈。
❄️ 冷启动(特别是无服务器)
在Vercel或AWS Lambda等无服务器平台上使用SSR?准备好冷启动延迟,特别是如果你不使用边缘函数或保持函数温暖。
🧠 更多的DevOps复杂性
SSR引入了基础设施开销:缓存策略、CDN配置、监控服务器端错误等。这不仅仅是"扔到Netlify上就忘了"
💻 更重的服务器负载
在服务器上渲染每个请求会增加CPU使用率,特别是在高流量页面上。没有适当的缓存(例如,全页或API响应缓存),成本可能快速上升。
🧩 水合开销
在HTML绘制后,客户端仍然需要水合页面以启用交互性,所以SSR并没有从等式中移除JS;它只是重新分配了一些责任。
总结
🔍 SSR vs CSR 快速比较
特性/标准
SSR(服务端渲染)
CSR(客户端渲染)
初始加载速度
由于预渲染HTML,首次绘制时间(TTFP)更快
TTFP较慢;JS必须先加载、解析和执行
SEO友好性
优秀 内容默认可爬取
⚠️ 需要额外设置(水合、SSR回退、预渲染)
个性化
通过服务器逻辑在发送HTML前轻松实现
需要在加载后客户端获取
秘密处理
安全(令牌/密钥保留在服务器上)
暴露秘密有风险
客户端设备负载
更轻 服务器做繁重工作
更重 浏览器必须渲染和水合
运营复杂性
更高 需要服务器基础设施、缓存、冷启动处理
更低 静态托管可能
后续导航
全页重新加载,除非添加混合/水合
平滑的SPA式过渡
交互性
加载后需要水合
JS加载后完全交互
最适合
SEO页面、落地页、包含敏感数据的仪表板
SPA、内部工具、高度交互的UI
结论:有目的地使用SSR
SSR很强大——但不是一刀切的解决方案。
在以下情况使用它:
你关心SEO和快速首次绘制。
你正在处理敏感数据或想要减少客户端API调用。
个性化或实时新鲜度很重要。
你想要保持秘密的秘密。
不要仅仅因为它很时髦就使用它。理解权衡,评估你的应用需要什么,并有目的地进行架构设计,而不是追逐流行词。
TL;DR:SSR对SEO、个性化和性能很好,但带来复杂性。在它解决真正问题的地方使用它,而不仅仅是因为它很闪亮。
如果你已经读到这里,那么我已经做出了令人满意的努力来保持你的阅读。请友好地留下任何评论或分享更正。
深入理解SSR
1. SSR的工作原理
基本流程
  1. // 传统CSR流程
  2. // 1. 浏览器请求页面
  3. // 2. 服务器返回空的HTML + JavaScript
  4. // 3. 浏览器执行JavaScript
  5. // 4. JavaScript获取数据
  6. // 5. 渲染内容

  7. // SSR流程
  8. // 1. 浏览器请求页面
  9. // 2. 服务器获取数据
  10. // 3. 服务器渲染HTML
  11. // 4. 服务器返回完整HTML
  12. // 5. 浏览器显示内容
  13. // 6. JavaScript水合页面(可选)
javascript
水合过程
  1. // 水合示例(React)
  2. // 服务器端渲染的HTML
  3. <div id="app">
  4. <h1>Hello, John!h1>
  5. <button>Click mebutton>
  6. div>

  7. // 客户端水合后
  8. // JavaScript接管,添加事件监听器
  9. document.querySelector('button').addEventListener('click', () => {
  10. console.log('Button clicked!');
  11. });
javascript
2. SSR实现示例
Next.js SSR示例
  1. // pages/index.js
  2. export default function Home({ data }) {
  3. return (
  4. <div>
  5. <h1>Welcome to our siteh1>
  6. <ul>
  7. {data.map(item => (
  8. <li key={item.id}>{item.title}li>
  9. ))}
  10. ul>
  11. div>
  12. );
  13. }

  14. // 服务器端数据获取
  15. export async function getServerSideProps() {
  16. const res = await fetch('https://api.example.com/data');
  17. const data = await res.json();

  18. return {
  19. props: {
  20. data,
  21. },
  22. };
  23. }
javascript
Nuxt.js SSR示例
  1. // pages/index.vue
  2. <template>
  3. <div>
  4. <h1>{{ title }}h1>
  5. <ul>
  6. <li v-for="post in posts" :key="post.id">
  7. {{ post.title }}
  8. li>
  9. ul>
  10. div>
  11. template>

  12. <script>
  13. export default {
  14. async asyncData({ $axios }) {
  15. const posts = await $axios.$get('/api/posts');
  16. return { posts };
  17. },
  18. data() {
  19. return {
  20. title: 'My Blog'
  21. };
  22. }
  23. };
  24. script>
javascript
3. 性能优化策略
缓存策略
  1. // 页面级缓存
  2. export async function getServerSideProps({ req, res }) {
  3. // 检查缓存
  4. const cached = await redis.get(`page:${req.url}`);
  5. if (cached) {
  6. return JSON.parse(cached);
  7. }

  8. // 获取数据
  9. const data = await fetchData();
  10. // 缓存结果(5分钟)
  11. await redis.setex(`page:${req.url}`, 300, JSON.stringify({
  12. props: { data }
  13. }));

  14. return { props: { data } };
  15. }
javascript
数据预取
  1. // 并行数据获取
  2. export async function getServerSideProps() {
  3. const [users, posts, comments] = await Promise.all([
  4. fetch('/api/users'),
  5. fetch('/api/posts'),
  6. fetch('/api/comments')
  7. ]);

  8. return {
  9. props: {
  10. users: await users.json(),
  11. posts: await posts.json(),
  12. comments: await comments.json()
  13. }
  14. };
  15. }
javascript
4. 错误处理
服务器端错误处理
  1. export async function getServerSideProps({ res }) {
  2. try {
  3. const data = await fetchData();
  4. return { props: { data } };
  5. } catch (error) {
  6. // 记录错误
  7. console.error('SSR Error:', error);
  8. // 返回错误页面
  9. res.statusCode = 500;
  10. return {
  11. props: {
  12. error: 'Something went wrong',
  13. data: null
  14. }
  15. };
  16. }
  17. }
javascript
客户端错误边界
  1. // React错误边界
  2. class ErrorBoundary extends React.Component {
  3. constructor(props) {
  4. super(props);
  5. this.state = { hasError: false };
  6. }

  7. static getDerivedStateFromError(error) {
  8. return { hasError: true };
  9. }

  10. render() {
  11. if (this.state.hasError) {
  12. return <h1>Something went wrong.h1>;
  13. }

  14. return this.props.children;
  15. }
  16. }
javascript
5. SEO优化
元标签管理
  1. // Next.js Head组件
  2. import Head from 'next/head';

  3. export default function BlogPost({ post }) {
  4. return (
  5. <>
  6. <Head>
  7. <title>{post.title}title>
  8. <meta name="description" content={post.excerpt} />
  9. <meta property="og:title" content={post.title} />
  10. <meta property="og:description" content={post.excerpt} />
  11. <meta property="og:image" content={post.featuredImage} />
  12. <link rel="canonical" href={`https://example.com/blog/${post.slug}`} />
  13. Head>
  14. <article>
  15. <h1>{post.title}h1>
  16. <div dangerouslySetInnerHTML={{ __html: post.content }} />
  17. article>
  18. );
  19. }
javascript
结构化数据
  1. // JSON-LD结构化数据
  2. export default function Product({ product }) {
  3. const structuredData = {
  4. "@context": "https://schema.org",
  5. "@type": "Product",
  6. "name": product.name,
  7. "description": product.description,
  8. "price": product.price,
  9. "image": product.image
  10. };

  11. return (
  12. <>
  13. type="application/ld+json"
  14. dangerouslySetInnerHTML={{
  15. __html: JSON.stringify(structuredData)
  16. }}
  17. />
  18. {/* 产品内容 */}
  19. );
  20. }
javascript
6. 安全考虑
敏感数据处理
  1. // 安全的API调用
  2. export async function getServerSideProps({ req }) {
  3. // 服务器端环境变量
  4. const apiKey = process.env.API_KEY;
  5. const response = await fetch('https://api.example.com/data', {
  6. headers: {
  7. 'Authorization': `Bearer ${apiKey}`,
  8. 'Content-Type': 'application/json'
  9. }
  10. });

  11. const data = await response.json();

  12. // 不要将敏感数据发送到客户端
  13. const sanitizedData = {
  14. id: data.id,
  15. title: data.title,
  16. // 排除敏感字段如apiKey, password等
  17. };

  18. return {
  19. props: {
  20. data: sanitizedData
  21. }
  22. };
  23. }
javascript
XSS防护
  1. // 安全的HTML渲染
  2. export default function BlogPost({ post }) {
  3. return (
  4. <div>
  5. <h1>{post.title}h1>
  6. {/* 使用dangerouslySetInnerHTML时要小心 */}
  7. <div
  8. dangerouslySetInnerHTML={{
  9. __html: DOMPurify.sanitize(post.content)
  10. }}
  11. />
  12. div>
  13. );
  14. }
javascript
7. 监控和分析
性能监控
  1. // 服务器端性能监控
  2. export async function getServerSideProps({ req, res }) {
  3. const startTime = Date.now();
  4. try {
  5. const data = await fetchData();
  6. // 记录性能指标
  7. const duration = Date.now() - startTime;
  8. console.log(`SSR duration: ${duration}ms`);
  9. // 发送到监控服务
  10. await analytics.track('ssr_performance', {
  11. page: req.url,
  12. duration,
  13. success: true
  14. });

  15. return { props: { data } };
  16. } catch (error) {
  17. const duration = Date.now() - startTime;
  18. await analytics.track('ssr_error', {
  19. page: req.url,
  20. duration,
  21. error: error.message
  22. });
  23. throw error;
  24. }
  25. }
javascript
用户体验监控
  1. // 客户端性能监控
  2. useEffect(() => {
  3. // 测量首次内容绘制
  4. const observer = new PerformanceObserver((list) => {
  5. for (const entry of list.getEntries()) {
  6. if (entry.name === 'first-contentful-paint') {
  7. analytics.track('fcp', {
  8. value: entry.startTime,
  9. page: window.location.pathname
  10. });
  11. }
  12. }
  13. });
  14. observer.observe({ entryTypes: ['paint'] });
  15. }, []);
javascript
实际应用场景
1. 电商网站
  1. // 产品页面SSR
  2. export async function getServerSideProps({ params }) {
  3. const { productId } = params;
  4. // 获取产品数据
  5. const product = await getProduct(productId);
  6. // 获取相关产品
  7. const relatedProducts = await getRelatedProducts(product.category);
  8. // 获取用户评价
  9. const reviews = await getProductReviews(productId);
  10. return {
  11. props: {
  12. product,
  13. relatedProducts,
  14. reviews
  15. }
  16. };
  17. }
javascript
2. 新闻网站
  1. // 新闻文章页面
  2. export async function getServerSideProps({ params, query }) {
  3. const { slug } = params;
  4. const { page = 1 } = query;
  5. // 获取文章内容
  6. const article = await getArticle(slug);
  7. // 获取最新新闻
  8. const latestNews = await getLatestNews();
  9. // 获取相关文章
  10. const relatedArticles = await getRelatedArticles(article.tags);
  11. return {
  12. props: {
  13. article,
  14. latestNews,
  15. relatedArticles,
  16. currentPage: parseInt(page)
  17. }
  18. };
  19. }
javascript
3. 企业仪表板
  1. // 仪表板页面
  2. export async function getServerSideProps({ req }) {
  3. // 验证用户身份
  4. const user = await authenticateUser(req);
  5. if (!user) {
  6. return {
  7. redirect: {
  8. destination: '/login',
  9. permanent: false
  10. }
  11. };
  12. }
  13. // 获取用户数据
  14. const [stats, recentActivity, notifications] = await Promise.all([
  15. getUserStats(user.id),
  16. getRecentActivity(user.id),
  17. getNotifications(user.id)
  18. ]);
  19. return {
  20. props: {
  21. user,
  22. stats,
  23. recentActivity,
  24. notifications
  25. }
  26. };
  27. }
javascript
总结
SSR是一个强大的工具,但需要根据具体需求来决定是否使用。关键是要理解:
使用SSR的最佳场景:
SEO关键页面:博客、产品页面、营销页面
个性化内容:用户仪表板、个性化推荐
敏感数据处理:包含API密钥或用户数据的页面
性能优化:慢速网络或低端设备
实时内容:新闻、股票、社交媒体
避免SSR的场景:
高度交互的应用:复杂的SPA应用
内部工具:不需要SEO的管理界面
简单的静态内容:可以使用SSG
资源受限:没有足够的服务器资源
记住:选择正确的渲染策略是架构决策的核心部分。SSR不是万能的,但它确实在特定场景下提供了巨大的价值。