🚀 React 自定义 SSR 深度解析

从原理到实现,全面理解 Server-Side Rendering

React 19 Koa.js React Query Webpack 5

项目概述

🎯 为什么要自定义 SSR?

Next.js 虽好,但在大型项目中面临问题:

  • 性能问题:冷启动时间可达 4S+
  • 稳定性:不支持服务降级到 CSR
  • 安全性:存在多个 CVE 漏洞
  • 可控性:闭源趋势,高级功能绑定 Vercel

✨ SSR 解决了什么问题?

  • 性能优化:首屏渲染更快,FCP 更短
  • SEO 友好:搜索引擎可直接抓取内容
  • 用户体验:无白屏,渐进式加载

核心架构

SSR Architecture

💡 同构渲染的核心思想

同一套 React 代码在服务端和客户端运行:
服务端:StaticRouter + renderToPipeableStream
客户端:BrowserRouter + hydrateRoot

SSR 渲染流程

1

路由匹配

matchRoutes()

2

数据预取

prefetchQuery

3

脱水

dehydrate()

4

流式渲染

renderToPipeableStream

5

水合

hydrateRoot()

Data Flow

Webpack 双入口构建

Webpack Dual Build

🖥️ Server Bundle

配置
入口 app/server/server.ts
target node
output commonjs
样式处理 ignore-loader

🌐 Client Bundle

配置
入口 app/client/index.tsx
target browserslist
代码分割 @loadable/component
样式处理 MiniCssExtractPlugin
config/webpack.config.js
// 服务端配置
server: (env) => ({
  target: "node",
  entry: { 
    server: resolve("app/server/server.ts") 
  },
  externalsPresets: { node: true },
  output: { libraryTarget: "commonjs" },
  module: { 
    rules: [{ 
      test: /\.(less|css|svg)$/, 
      loader: "ignore-loader" 
    }] 
  }
})

// 客户端配置
client: (env) => ({
  target: "browserslist",
  entry: { client: resolve("app/client/index.tsx") },
  plugins: [
    new LoadablePlugin(), 
    new MiniCssExtractPlugin()
  ]
})

React 18 流式渲染

Stream Rendering
app/server/stream/response.ts
import { renderToPipeableStream } from "react-dom/server";

export const response = (ctx, jsx, startTemplate, endTemplate) => {
  return new Promise((resolve, reject) => {
    const { pipe } = renderToPipeableStream(jsx, {
      
      // 1️⃣ Shell 准备就绪 - 发送 HTML 头部
      onShellReady() {
        ctx.res.write(getStartTemplate(startTemplate));
      },
      
      // 2️⃣ 所有内容就绪 - 流式发送 + 结尾
      onAllReady() {
        pipe(ctx.res);
        ctx.res.end(getEndTemplate(endTemplate));
      },
      
      // ❌ 错误处理
      onShellError(error) { 
        ctx.status = 500; 
        reject(error); 
      }
    });
  });
};

⚡ 流式渲染的优势

TTFB 更快:用户在数据加载完成前就能看到页面骨架
渐进式加载:内容逐步显示,无需等待全部完成

服务降级机制

SSR Degradation
服务端注入标志
// 服务端渲染时注入标志
`<script id="__APP_FLAG__">
  {"isSSR": true}
</script>`
客户端检测
const getTradeFlag = () => {
  const el = document.querySelector("#__APP_FLAG__");
  if (!el) return { isSSR: false };
  return JSON.parse(el.textContent);
};
app/client/index.tsx - 降级逻辑
const renderWithErrorBoundary = () => {
  try {
    if (tradeFlag.isSSR) {
      // ✅ SSR 模式:水合已有 DOM
      loadableReady(() => {
        hydrateRoot(root, <ClientApp />);
      });
    } else {
      // ⚡ CSR 模式:完整客户端渲染
      createRoot(root).render(<ClientApp />);
    }
  } catch (error) {
    // 🆘 水合失败也降级
    createRoot(root).render(<ClientApp />);
  }
};

⚠️ 降级触发条件

场景 触发 结果
正常 SSR isSSR: true hydrateRoot()
Node.js 挂掉 标志不存在 createRoot()
水合报错 catch 捕获 createRoot()

核心源码解析

📦 服务端路由入口

app/server/index.tsx
router.get("(.*)", async (ctx) => {
  const extractor = new ChunkExtractor({ statsFile });
  const queryClient = new QueryClient();
  
  // 数据预取
  const { dehydratedState } = await prefetch(ctx, queryClient);
  
  // 渲染
  const { jsx, helmetContext } = renderApp(ctx, queryClient, extractor, dehydratedState);
  
  // 流式响应
  await response(ctx, jsx, { helmetContext, extractor }, { dehydratedState });
});

🛤️ 路由配置

src/routes/index.tsx
const routes = [
  {
    path: "/",
    element: <Index />,
    children: [
      {
        path: ":locales/home",
        element: <Home />,
        queryKey: [PrefetchKeys.HOME],
        loadData: HomeService.getList
      }
    ]
  }
];

技术栈

层级 技术 用途
前端 React 19 UI 渲染
服务端 Koa.js HTTP 服务
数据 React Query 预取/缓存
构建 Webpack 5 双入口打包

关键要点

🔄

同构渲染

同一份代码双端运行

📊

数据同步

Dehydrate/Hydrate

🌊

流式传输

renderToPipeableStream

🛡️

服务降级

SSR→CSR 自动切换