如何构建一个支持SSR服务端渲染的React应用?

来源:JS脚本作者:南京GEO公司头衔:草根站长
导读:本期聚焦于南京GEO公司创作的《如何构建一个支持SSR服务端渲染的React应用?》,敬请观看详情。页面打开半天白屏、SEO收录效果差、首屏体验不尽如人意,这些问题往往指向同一个答案:你的React应用缺少服务端渲染。本文将围绕React SSR展开,先讲清楚服务端渲染与客户端渲染的本质区别,说明浏览器拿到HTML的完整过程,再介绍两种主流落地方式:使用Next.js框架快速搭建,以及用Express配合renderToString手写一个最小可用的SSR服务。文中包含同构代码的实现细节、水合阶段容易踩的坑,以及路由、数据预取在服务端的处理方案,帮助你在真实项目中顺利落地SSR。

SPA(单页应用)给前端带来了极佳的交互体验,但它天生有一个短板:服务端返回的只是一个几乎空白的HTML壳子,页面内容完全依赖浏览器下载并执行JavaScript之后才能渲染出来。这直接导致首屏白屏时间较长,搜索引擎爬虫也难以抓取有效内容。服务端渲染(SSR)正是为了解决这些问题而存在的方案:让服务器直接输出渲染完成的HTML,浏览器拿到就能展示,之后再由客户端接管交互。本文将从原理到实践,完整讲解如何构建一个支持SSR的React应用。

如何构建一个支持SSR服务端渲染的React应用?

一、先弄清楚SSR与CSR的本质区别

客户端渲染(CSR)的流程是这样的:浏览器请求页面,服务器返回一个只包含<div id="root"></div>和一个<script>标签的HTML文件,浏览器解析后下载JS bundle,执行React代码,由ReactDOM.createRoot把组件树渲染到真实DOM上。也就是说,用户在JS下载执行完成之前,看到的是一片空白。

服务端渲染(SSR)则把组件渲染这一步提前到了服务器上。服务器执行React组件代码,调用renderToString把组件树转成HTML字符串,直接拼进响应体返回给浏览器。浏览器收到的是带着完整内容的HTML,可以立即绘制,SEO爬虫也能直接读到内容。

但SSR并不是终点。服务器输出的HTML是静态的,没有事件绑定,没有交互能力。所以浏览器侧还需要再执行一次React,把事件和状态附加到已有的DOM上,这个过程叫做水合。理解“服务端渲染字符串 + 客户端水合”这两阶段,是掌握React SSR的关键。

二、方案选型:Next.js还是自己搭

如果项目从零开始,且希望快速获得SSR能力,Next.js几乎是默认选择。它内置了路由系统(基于文件系统的pages或app目录)、数据预取、代码分割、图片优化等能力,开发者几乎不需要关心SSR的底层细节。安装也非常简单:

npx create-next-app@latest my-ssr-app
cd my-ssr-app
npm run dev

在app目录下写一个页面组件,Next.js会自动对它做服务端渲染。如果某些页面需要客户端交互,只需在组件顶部加上'use client'指令声明即可,框架会处理好边界。

但Next.js的约定式路由和内部机制也带来一定学习成本和黑盒感。如果你的需求是给已有项目加SSR,或者希望完全掌控渲染流程,自己用Express加React手写一套SSR服务也是可行的,接下来就动手实现一个最小可用版本。

三、手写一个Express加React的SSR服务

首先搭建项目结构。核心思路是代码同构:同一份组件代码,在服务端被renderToString执行,在客户端被hydrateRoot执行。项目通常分三个入口:共享的App组件、服务端入口、客户端入口。

共享的App组件保持纯净,不要在组件顶层直接访问windowdocument,因为服务器上没有这些对象。组件示例如下:

import React from 'react';

export default function App({ data }) {
  return (
    <div>
      <h1>Hello SSR</h1>
      <p>服务端传入的数据:{data}</p>
    </div>
  );
}

服务端入口使用Express,把渲染出的HTML字符串拼接到模板中返回。注意把初始数据序列化后注入window.__INITIAL_DATA__,供客户端水合时复用,避免二次请求:

import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App';

const app = express();

app.get('/', (req, res) => {
  const data = '来自服务端的数据';
  const html = renderToString(React.createElement(App, { data }));
  res.send(`
    <!DOCTYPE html>
    <html>
      <head><title>React SSR</title></head>
      <body>
        <div id="root">${html}</div>
        <script>window.__INITIAL_DATA__ = ${JSON.stringify({ data })};</script>
        <script src="/bundle.js"></script>
      </body>
    <html>
  `);
});

app.listen(3000, () => console.log('SSR server on 3000'));

客户端入口则调用hydrateRoot而不是createRoothydrateRoot会复用服务端已经生成的DOM节点,只附加事件监听,而不是销毁重建:

import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import App from './App';

const initialData = window.__INITIAL_DATA__ || { data: '' };

hydrateRoot(
  document.getElementById('root'),
  React.createElement(App, initialData)
);

构建方面需要用Webpack(或Vite)分别打包客户端和两端共用的代码,通常配置两个入口,服务端产物以CommonJS格式输出给Node直接require,客户端产物输出给浏览器加载。

四、路由与数据预取的服务端处理

SSR下的路由不能只依赖浏览器地址栏。服务端需要根据请求的URL决定渲染哪个组件,常用做法是配合StaticRouter。服务端读取req.url传入路由上下文,客户端则继续使用BrowserRouter

import { StaticRouter } from 'react-router-dom/server';

const html = renderToString(
  <StaticRouter location={req.url}>
    <App />
  </StaticRouter>
);

数据预取是SSR中另一个重点。常见模式是在路由配置上挂一个静态方法loadData,服务端匹配到路由后先调用它获取数据,拿到结果再传给renderToString。这样首屏HTML里就带有真实数据,无需等待客户端请求返回。

同时要注意生命周期差异:服务端只执行到componentDidMount之前的部分,useEffect在服务端完全不执行。所有依赖浏览器API的逻辑,要么放进useEffect,要么通过typeof window !== 'undefined'做环境判断。

五、水合常见坑与性能建议

水合失败最常见的原因是服务端输出与客户端首次渲染结果不一致。比如代码里写了Date.now()或随机数,两端渲染结果必然不同,React会在控制台报水合警告,甚至整棵树重新渲染。解决办法是把这类动态值放到useEffect中生成,保证两端首渲一致。

另一个坑是HTML结构错误,例如在<p>标签内嵌套了<div>,浏览器会自动纠正结构,导致客户端看到的DOM与服务端字符串对不上。这类问题排查起来比较隐蔽,建议使用validateDOMNesting相关提示仔细检查。

性能方面有几点建议:第一,对组件使用React.memo减少水合阶段的重复渲染开销;第二,利用流式渲染(renderToPipeableStream)让HTML分块尽早到达浏览器;第三,把首屏必需的CSS内联进HTML,避免渲染阻塞;第四,对非首屏组件做懒加载,减小客户端bundle体积。

总结一下,构建React SSR应用的核心是理解同构思想:一份代码,两端执行,服务端负责出HTML,客户端负责水合接管交互。如果追求开发效率直接选Next.js,如果想深度定制就用Express手搭。无论哪种路线,处理好数据预取和水合一致性,SSR就能稳定地为你带来更快的首屏和更好的SEO表现。

React SSR服务端渲染Next.js修改时间:2026-09-02 23:47:23

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260902/49211.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。