大前端技术学习(一):客户端&服务端渲染技术

两种渲染方式的“性格分析”

CSR:那个自由奔放的艺术家

CSR(客户端渲染)有点像一位自由艺术家——它在用户的浏览器里挥洒创意,动态地创造一切。

// 这就是CSR的核心哲学
function renderEverythingInBrowser() {
  // 1. 先给用户一个空白画布
  const app = document.getElementById('app');
  app.innerHTML = '<div class="spinner">加载中...</div>';
  
  // 2. 慢慢准备颜料(数据)
  fetch('/api/data').then(data => {
    // 3. 开始创作(渲染)
    app.innerHTML = createBeautifulUI(data);
  });
}

CSR的真实体验

  • 首次访问:看到loading动画 → 等待 → 页面突然完整呈现
  • 后续导航:丝般顺滑,就像在用桌面应用
  • 开发体验:前后端完全分离,前端可以玩得很嗨

但我记得有一次,我们的网站在Google搜索里完全找不到商品详情页。SEO团队找上门来,那场面有点尴尬。后来才知道,Google的爬虫虽然能执行JavaScript,但时间有限,我们那个页面要5秒才能渲染完,爬虫早就走了。

SSR:那位稳重的工匠

SSR(服务端渲染)更像一位老工匠,在把作品交给你之前,已经精心打磨好了每一个细节。

// SSR的工作方式
async function handleRequest(req, res) {
  // 1. 收到请求后,立即开始工作
  const user = await getUser(req);
  const products = await getProducts();
  
  // 2. 在服务器端就把一切都准备好
  const html = `
    <!DOCTYPE html>
    <html>
      <head><title>商品列表</title></head>
      <body>
        <h1>欢迎回来,${user.name}!</h1>
        <div class="products">
          ${products.map(p => `<div>${p.name} - ¥${p.price}</div>`).join('')}
        </div>
      </body>
    </html>
  `;
  
  // 3. 把完整的作品交付给用户
  res.send(html);
}

SSR的温暖之处

  • 打开页面:内容瞬间呈现,没有“等待感”
  • 分享链接:社交媒体能抓取到完整的页面预览
  • 低端设备:老款手机也能快速看到内容

不过SSR也有自己的烦恼。有一次促销活动,流量暴增,我们的Node服务器因为要为每个请求渲染页面,CPU直接飙到100%,页面打开时间从200ms涨到了5秒。

现代开发者的“中庸之道”

现在的我,已经不再非黑即白地看待这个问题。就像做菜,CSR和SSR都是食材,关键看你怎么搭配。

场景一:营销落地页 → SSG

对于公司官网、产品介绍页这些内容不怎么变动的页面,我选择静态站点生成(SSG)

// Next.js的getStaticProps
export async function getStaticProps() {
  // 构建时获取数据,生成静态HTML
  const pageData = await fetchPageData();
  
  return {
    props: { pageData },
    // 每24小时重新生成一次
    revalidate: 86400
  };
}

上次我们公司官网从WordPress迁移到Next.js SSG,加载时间从2.8秒降到了0.4秒,而且再也不用担心被黑客攻击了——静态文件太安全了。

场景二:用户仪表盘 → CSR

对于需要大量交互、实时数据的后台管理系统,我坚持用CSR

// 后台管理系统的典型结构
function AdminDashboard() {
  const [realTimeData, setRealTimeData] = useState(null);
  
  // WebSocket实时更新数据
  useEffect(() => {
    const ws = new WebSocket('wss://api.example.com/realtime');
    ws.onmessage = (event) => {
      setRealTimeData(JSON.parse(event.data));
    };
  }, []);
  
  // 各种交互式图表和表单
  return (
    <div>
      <RealTimeChart data={realTimeData} />
      <InteractiveDataGrid />
      <ComplexFilterPanel />
    </div>
  );
}

这里用SSR意义不大,因为每个用户看到的内容都不一样,而且交互极其复杂。

场景三:电商详情页 → SSR + CSR混合

这是最有趣的模式。我们为电商产品页设计的方案:

// 混合渲染示例
function ProductPage({ initialProductData }) {
  // 首屏用SSR渲染的关键信息
  return (
    <div>
      {/* SSR渲染的部分 - 立即显示 */}
      <div className="product-header">
        <h1>{initialProductData.name}</h1>
        <img src={initialProductData.image} alt={initialProductData.name} />
        <div className="price">¥{initialProductData.price}</div>
      </div>
      
      {/* 懒加载的交互部分 */}
      <Suspense fallback={<div>加载评论中...</div>}>
        <ProductReviews productId={initialProductData.id} />
      </Suspense>
      
      {/* 只在客户端渲染的推荐模块 */}
      <ClientOnly>
        <ProductRecommendations />
      </ClientOnly>
    </div>
  );
}

// 服务端获取初始数据
export async function getServerSideProps(context) {
  const productId = context.params.id;
  const productData = await fetchProductData(productId);
  
  return {
    props: {
      initialProductData: productData
    }
  };
}

这样做的好处是:

  1. 用户瞬间看到产品核心信息(SSR的优点)
  2. 评论区等次要内容慢慢加载
  3. 个性化推荐只在客户端计算,不增加服务器负担

实战中的那些“坑”与“解”

坑1:SSR中的window未定义

// 错误示范
function MyComponent() {
  // 服务端渲染时会报错:window is not defined
  const screenWidth = window.innerWidth;
  
  return <div>宽度:{screenWidth}px</div>;
}

// 正确做法
function MyComponent() {
  const [screenWidth, setScreenWidth] = useState(0);
  
  useEffect(() => {
    // 只在客户端执行
    setScreenWidth(window.innerWidth);
  }, []);
  
  return <div>宽度:{screenWidth || '加载中...'}px</div>;
}

坑2:CSR的首屏空白时间太长

解决方案

  1. 代码分割:让首屏只加载必要的代码
  2. 骨架屏:给用户一个加载预期
  3. 资源预加载:悄悄加载下一页的资源
// 骨架屏示例
function ProductList() {
  const [products, setProducts] = useState(null);
  
  if (!products) {
    return (
      <div className="skeleton-container">
        {[...Array(10)].map((_, i) => (
          <div key={i} className="product-skeleton">
            <div className="skeleton-image"></div>
            <div className="skeleton-text"></div>
            <div className="skeleton-text short"></div>
          </div>
        ))}
      </div>
    );
  }
  
  return <RealProductList products={products} />;
}

坑3:SSR服务器压力大

我们的解决方案

  • 缓存:给经常访问的页面加缓存
  • 降级策略:流量太大时,自动降级为CSR
  • 边缘渲染:把SSR放到CDN边缘节点
// 简单的缓存中间件
const ssrCache = new Map();

app.get('/product/:id', async (req, res) => {
  const cacheKey = `product-${req.params.id}`;
  
  // 检查缓存
  if (ssrCache.has(cacheKey)) {
    const { html, timestamp } = ssrCache.get(cacheKey);
    
    // 缓存5分钟内有效
    if (Date.now() - timestamp < 5 * 60 * 1000) {
      return res.send(html);
    }
  }
  
  // 重新渲染
  const html = await renderProductPage(req.params.id);
  
  // 更新缓存
  ssrCache.set(cacheKey, {
    html,
    timestamp: Date.now()
  });
  
  res.send(html);
});

我的技术选型心法

经过这些年的实践,我总结了一个简单的决策框架:

function 选择渲染策略(项目需求) {
  if (需求.需要SEO && 需求.内容动态) {
    return 'SSR'; // 如新闻网站、电商详情页
  }
  
  if (需求.需要SEO && !需求.内容动态) {
    return 'SSG'; // 如公司官网、博客
  }
  
  if (!需求.需要SEO && 需求.高度交互) {
    return 'CSR'; // 如后台管理系统、Web应用
  }
  
  // 其他情况,考虑混合方案
  return 'SSR+CSR混合';
}

但说实话,现在有了Next.js、Nuxt.js这些框架,我们不必从一开始就做艰难的选择。它们支持各种渲染模式,你甚至可以在同一个应用的不同页面使用不同的策略。

未来:React Server Components带来的思考

最近我在试验React Server Components,感觉它可能改变游戏规则。让我眼前一亮的是:

// Server Component - 只在服务端运行
async function ProductDetails({ productId }) {
  // 可以直接访问数据库,不需要API层
  const product = await db.products.findUnique({
    where: { id: productId }
  });
  
  // 这个组件不会被打包到客户端bundle中
  return (
    <div>
      <h1>{product.name}</h1>
      <ProductReviews productId={productId} />
    </div>
  );
}

// 在客户端组件中使用
'use client';

function ProductPage({ productId }) {
  return (
    <div>
      <Suspense fallback={<div>加载产品信息...</div>}>
        {/* 这个组件在服务端渲染,零客户端bundle */}
        <ProductDetails productId={productId} />
      </Suspense>
      
      {/* 这个是客户端交互组件 */}
      <AddToCartButton productId={productId} />
    </div>
  );
}

这让我思考:也许未来的前端开发,不再需要纠结于“在哪里渲染”,而是根据组件的特性自然选择执行环境。

写在最后:技术选择是一种权衡

回顾这些年前端渲染的演变,我意识到没有完美的解决方案,只有适合当前场景的选择。

  • 早期网站:都是SSR,因为那时候连JavaScript都不成熟
  • SPA时代:CSR成为主流,追求桌面应用般的体验
  • 现在:混合方案,根据需求选择最合适的工具

作为开发者,我们容易陷入“技术决定论”——认为某个技术一定优于另一个。但真实项目中,需要考虑的因素太多了:团队技能、项目规模、性能要求、SEO需求、维护成本...

所以我的建议是:

  1. 从简单开始:新项目可以从CSR开始,快速验证想法
  2. 按需优化:遇到具体问题(如SEO、首屏性能)时,再引入SSR
  3. 保持开放:新技术不断出现,保持学习但不要盲目跟风

最后分享一个让我释怀的观点:Web的本质是渐进增强。无论选择哪种渲染方式,都要确保基础内容能被快速访问,然后在此基础上增强体验。

posted @ 2025-12-21 10:54  灰色飘零  阅读(2)  评论(0)    收藏  举报