1. 从浏览器到服务器:我们需要SSR的三大理由

打开淘宝商品页时,你会先看到完整的商品详情再加载图片,这背后正是服务端渲染(SSR)在起作用。相比传统React客户端渲染(CSR),SSR最大的优势是将首屏内容直接输出到HTML文档中:

  1. SEO优化:爬虫直接读取服务端生成的HTML内容
  2. 首屏加速:用户无需等待JavaScript加载完成就能看到内容
  3. 数据预取:服务端在渲染时就已经完成数据加载

我们来看一个典型的CSR应用加载流程:

// CSR加载流程示意(React + create-react-app)
function App() {
  const [data, setData] = useState(null); // 需要等待数据请求
  
  useEffect(() => {
    fetch('/api/data').then(res => setData(res.json()));
  }, []);

  return data ? <Product data={data} /> : <LoadingSpinner />;
}
// 用户必须等待3个步骤:下载HTML -> 下载JS -> 发起数据请求

2. Next.js的SSR实现三板斧

在Next.js项目中,这三个核心API构成SSR技术栈:

  • getServerSideProps:为每个请求动态生成页面
  • getStaticProps:构建时生成静态页面
  • Dynamic Routes:动态路径的SSR支持

实战示例1:电商产品页的SSR实现

// pages/products/[id].js
export async function getServerSideProps(context) {
  // 从URL参数获取产品ID
  const { id } = context.params;
  
  // 服务端直接访问数据库
  const product = await db.products.findUnique({
    where: { id: parseInt(id) }
  });

  // 返回的props会传递给页面组件
  return { 
    props: { product }
  };
}

function ProductPage({ product }) {
  // 无需加载状态,数据直接可用
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}
// 这个页面会在每次请求时通过服务端获取最新数据并预渲染

3. 混合渲染策略:让动静结合的艺术

Next.js最大的优势是支持混合渲染,通过合理的路由配置实现动静分离:

路由类型 适用场景 实现方式
动态SSR页面 用户中心/实时数据 getServerSideProps
静态生成页面 博客/商品分类页 getStaticProps
增量静态更新 促销活动页/高频更新内容 revalidate 参数

实战示例2:带增量更新的新闻列表

// pages/news/index.js
export async function getStaticProps() {
  const newsList = await fetchNews();
  
  return {
    props: { newsList },
    // 每60秒重新生成页面
    revalidate: 60 
  };
}

function NewsPage({ newsList }) {
  // 首次访问是构建时的静态内容
  // 之后访问每60秒刷新一次
  return (
    <div>
      {newsList.map(news => (
        <article key={news.id}>
          <h2>{news.title}</h2>
          <p>{news.excerpt}</p>
        </article>
      ))}
    </div>
  );
}
// 通过ISR实现高性能和时效性的平衡

4. 深水区:SSR项目中的性能优化

实施SSR时需要注意的三大性能陷阱:

4.1 数据获取瀑布流问题

// 错误示例:嵌套的数据获取
export async function getServerSideProps() {
  const user = await fetchUser();  // 第一个请求
  const orders = await fetchOrders(user.id); // 依赖第一个结果
  return { props: { user, orders } };
}
// 正确做法:并行获取
Promise.all([fetchUser(), fetchOrders()]);

4.2 合理使用缓存策略

// 自定义缓存头示例
export async function getServerSideProps({ res }) {
  res.setHeader(
    'Cache-Control',
    'public, max-age=60, stale-while-revalidate=300'
  );
  
  // ...数据获取逻辑
}
// 对不常变化的数据添加CDN缓存

4.3 服务端/客户端状态同步

// 使用全局状态管理器保持同步
import { useStore } from '../store';

function UserProfile({ serverData }) {
  const { setUser } = useStore();
  
  // 将服务端数据注入客户端状态
  useEffect(() => {
    if(serverData) {
      setUser(serverData.user);
    }
  }, [serverData]);

  return <div>{serverData.user.name}</div>;
}
// 确保两端状态一致性

5. 那些年我们踩过的SSR的坑

实际项目中常见的四个问题及解决方案:

5.1 环境差异问题

// 错误示例:在服务端访问window对象
function Component() {
  const [width, setWidth] = useState(window.innerWidth);
}
// 正确解决方案:
typeof window !== 'undefined' && window.innerWidth

5.2 内存泄漏防范

// 在getServerSideProps中及时清理资源
export async function getServerSideProps() {
  const controller = new AbortController();
  try {
    const data = await fetch('/api', {
      signal: controller.signal
    });
    return { props: { data } };
  } catch (err) {
    // 取消未完成的请求
    controller.abort();
  }
}

6. 选型指南:何时该用SSR?

适合SSR的应用场景:

  • 强SEO需求的官网、电商平台
  • 需要即时更新的新闻/社交媒体类应用
  • 首屏性能要求极高的移动端网站

不适合SSR的场景:

  • 内部管理系统(对SEO无需求)
  • 实时交互型应用(如在线绘图工具)
  • 基础架构不足(缺乏Node.js运维能力)