SSR、Hydration 这些词在 Web 前端领域非常常见开发者经常能接触到这个概念。但是这些是什么为什么怎么用过去我都没有深究下去关于 SSR我承认我之前只是“会用”而已。一、区分 CSR 还是 SSR回顾自己做的一些小玩意发现自己大部分都是用 Vite React TS 来构建项目是 CSR采用的是 SPA 场景Vite 默认是 SPA。虽说也用 Next.js 写过项目属于 SSR 支持但没有深究过里面的内容。如何区分启动项目以后检查页面源代码SSR会看到完整的内容而不是空的div idrootCSR浏览器几乎拿到的就是空的 HTML特性SPASSR首屏速度慢等 JS 下载执行快直接返回 HTML页面切换极快局部刷新较慢可能重新请求SEO差好服务器压力小只传数据大需要渲染页面复杂度低高典型场景后台系统、工具类应用博客、电商、内容型网站什么场景适合 SSR 内容型 C 端产品电商、资讯、博客需要 SEO 和首屏速度比较适合强交互的后台系统、内部工具就没必要硬上 SSR 了。核心判断标准很简单首屏速度和 SEO 对业务价值有多大如果答案是“不大”SSR 就是过度设计。SEO搜索引擎优化是一种让网站在搜索引擎结果中更加清晰也帮助我们将搜索结果更靠前的过程。搜索引擎爬取网页跟随页面之间的链接并索引找到的内容。搜索时搜索引擎会显示索引内容。爬行者遵守规则。如果你在为网站进行搜索引擎优化时密切关注这些规则则会为网站提供最好的机会以便在首批结果中显示增加流量和可能的收入用于电子商务和广告。二、为什么用 Next.js “没感觉”我觉得搭建一个脚手架用 Next.js 实现 SSR 确实是没有难度的。框架本身封装了很多方法用起来比较傻瓜写代码的方式和写普通 React SPA 几乎一样所以我“没感觉”所以 Next.js 的使用没有很深刻的体会。Next.js 框架保护了我这样的猪头。它作为框架把 SSR 的工程化难题全解决了开箱即用。从设计哲学的角度来说Next.js是非常成功了。又也许是因为自己的 toy 项目没感到 SSR 的好处——体量都比较小没有太深的感受反而觉得有点重并不是太好用。SSR 的特点是什么SSR 的代表框架有 React 生态的 Next.js 和 Vue 生态的 Nuxt。它的优点是首屏加载快用户能很快看到内容同时 SEO 友好搜索引擎可以直接抓取到完整的 HTML缺点也很明显服务器压力更大因为每次请求都要重新渲染页面而且整体复杂度高需要处理水合Hydration、DOM 结构匹配等一系列问题。按照这些特点来看之前我的猜想是有道理的。toy项目没有必要SSR自然也就感受不到。那么SSR 的真正价值场景是什么呢SSR 常应用在首屏依赖大量异步数据比如电商详情页、资讯详情页、SEO 直接决定业务流量C 端内容产品、弱网环境下 HTML 比 JSONJS 更早到达的情况下。而我的小项目SEO 无所谓首屏慢 0.5 秒也没人投诉。这种场景强行上 SSR反而是用复杂度换不存在的问题。框架无好坏场景才决定选择。渲染模式其实是一个光谱不是简单的二选一。从纯静态 HTML 到 SSG、ISR、SSR、流式 SSR、CSR再到部分水合、群岛架构中间有很多种组合也就衍生到一个混合渲染的概念这在现代框架中其实是常见的。现代趋势混合方案现在很多框架Next.js、Nuxt允许按页面或按组件选择不需要 SEO 的后台页面 → 用 SPA 模式需要首屏快、SEO 好的营销页/博客 → 用 SSR 模式这就是所谓的混合渲染。例如 Next.js 的use client一开始我以为是“只在客户端运行”的意思。但它其实是水合边界的声明不是运行环境的切换——只有你显式标记的组件才会被水合。标记了use client的组件服务端照样会执行一次来生成 HTML只是客户端需要再次激活。所以它并不解决服务端没有window的问题你依然要用useEffect或动态导入来绕过。再看 Next.js 的 App Router它默认就是 React Server Components连 JS 都不发给客户端只有显式标记use client的组件才会被水合。这种“部分水合”的思路其实更接近 Astro 的群岛架构。三、SSR 到底是怎么工作的简单说流程是用户访问 → 服务器在服务端就把组件渲染成完整 HTML → 浏览器直接看到完整页面 → 再下载 JS 补充交互 Hydration水合。如果用 React 官方的底层 API 来理解核心就是renderToString或者更高级的renderToPipeableStream流式 SSR。服务端把App /变成 HTML 字符串浏览器拿到后先展示再用hydrateRoot把事件绑上去。把组件变成 HTML这一步其实和 React 在浏览器里做的事一模一样只是运行的地方不同。浏览器里CSR组件代码 → React 计算虚拟 DOM → 变成真实 DOM → 挂到 div idroot 里服务器上SSR组件代码 → React 计算虚拟 DOM → 变成 HTML 字符串 → 直接拼到响应里返回服务器上不需要挂载 DOM只需要输出字符串。React 提供了专门的 API 来做这件事renderToString()把一个组件变成 HTML 字符串renderToPipeableStream()流式输出边渲染边发送所以“在服务端把组件渲染成完整 HTML”这句话底层就是调了renderToString(App /)。hydrateRoot的作用已经有了 HTML 了服务端生成的现在 JS 加载完了React 需要在浏览器里接管这个页面。如果不做任何事页面看起来是好的但按钮点不了输入框没反应——因为事件监听器还没绑上去。如果用传统的renderReact 会把现有的 DOM 全部删掉重新创建一份再把事件绑上去。这样效率低而且用户可能会看到页面闪烁。hydrateRoot的做法检查现有的 DOM 结构不删除、不重建直接在已有的 DOM 节点上挂载事件监听器把内部状态初始化好打个比方你收到了一辆已经组装好的车服务端给的 HTML技师hydrateRoot不需要把车拆了重装只需要把方向盘、刹车、油门这些操控装置接上车就能开了。This will hydrate the server HTML inside the browser DOM node with the React component for your app. Usually, you will do it once at startup. If you use a framework, it might do this behind the scenes for you.这样可以将浏览器 DOM 节点内的服务器 HTML 与应用的 React 组件进行水合。通常你会在启动时做一次。如果你用框架它可能在幕后帮你做到这一点。To hydrate your app, React will “attach” your components’ logic to the initial generated HTML from the server. Hydration turns the initial HTML snapshot from the server into a fully interactive app that runs in the browser.为了给你的应用提供水合React 会把组件的逻辑“附加”到服务器最初生成的 HTML 上。Hydration 将服务器上的初始 HTML 快照转化为一个在浏览器中运行的完全交互式应用。这里只是介绍了一下react 框架对于 ssr的做法更多细节还是具体结合项目代码或者源码。建议读一读各个框架关于 SSR Hydration的官方介绍文档。hydrateRoot – 反应 — hydrateRoot – React服务器端渲染 (SSR) | Vue.js 框架Getting Started: Server and Client Components | Next.js用最简单的代码对比纯 CSR与SSR Hydration纯 CSRimport{createRoot}fromreact-dom/clientimportAppfrom./App// 创建一个新的根节点从头渲染constrootcreateRoot(document.getElementById(root))root.render(App/)SSR Hydrationimport{hydrateRoot}fromreact-dom/clientimportAppfrom./App// 不创建新节点而是复用已有的 HTMLhydrateRoot(document.getElementById(root),App/)区别就这一行createRoot→hydrateRoot其他代码一模一样。写到这里发现没感觉的真相自然水落石出——框架把这一切都隐藏了。用 Next.js 的时候不需要手动调用renderToString不需要手动调用hydrateRoot只管写组件框架帮你做了。这也就是我之前说的“框架保护了我这样的猪头”——好处是省事坏处是不知道底层发生了什么。React的最小示例server.jsimportexpressfromexpressimport{renderToString}fromreact-dom/serverimportAppfrom./App.jsxconstappexpress()app.get(/,(req,res){consthtmlrenderToString(App/)res.send(!DOCTYPE html html body div idroot${html}/div script src/client.js/script /body /html)})app.listen(3000)client.jsimport{hydrateRoot}fromreact-dom/clientimportAppfrom./App.jsxhydrateRoot(document.getElementById(root),App/)页面是服务端生成的右键查看源代码能看到完整内容水合成功后页面可以交互如果 App 组件里写了Date.now()会报水合错误看到第三点也许会有疑问了为什么会报错误呢接下来就说说水合。四、关于 Hydration水合1. 服务端生成静态骨架 (Server-Side Rendering)动作当用户访问页面时服务器会快速获取数据将你的框架代码如 React、Vue渲染成一段完整的 HTML 字符串。结果用户浏览器接收到的是立即可见的完整 HTML 页面解决了传统 SPA 应用“白屏”和 SEO 难的问题。2. 客户端注入灵魂与交互 (Hydration)动作浏览器显示 HTML 后会加载 JS 文件。框架如 React 的hydrateRoot开始工作它不会重新创建整个 DOM那样会浪费性能而是找到现有的 DOM 节点在上面挂载事件监听器如onClick和绑定内部状态。结果原本“静态”的页面瞬间“活”了变成了一个可点击、可交互的完整单页应用SPA。一个生动的比喻水合就像给一辆已经组装好的顶级跑车静态 HTML安装发动机和方向盘JS 逻辑。你在展厅里第一眼就能看到它炫酷的外形快速首屏但只有技师把操控系统装进去后你才能真正点火开走它。一开始我以为水合就是给 DOM 挂上事件监听器后来发现没那么简单。水合时 React 到底在比什么不是比内容文本而是比 DOM 树结构。服务端生成的 HTML 结构必须和客户端第一次 render 生成的虚拟 DOM 结构完全一致。如果不一致React 会直接报错Text content did not match。更麻烦的后果水合失败后React 不会尝试修复而是直接丢弃服务端渲染的 HTML在客户端重新渲染整棵子树。降级为 CSR 行为。这意味着你付出了 SSR 的成本服务端渲染 网络传输却没有得到首屏收益甚至比纯 CSR 更慢。常见的水合失败场景代码级// ❌ 危险时间戳不一致functionBadComponent(){returndiv{Date.now()}/div}// ❌ 危险条件判断不一致functionBadComponent(){returndiv{typeofwindowobject?客户端:服务端}/div}// ✅ 安全把依赖浏览器的逻辑放到 useEffectfunctionGoodComponent(){const[timestamp,setTimestamp]useState(null)useEffect(()setTimestamp(Date.now()),[])returndiv{timestamp}/div}React 18 的选择性水合新版本支持用Suspense包裹组件优先激活用户正在交互的部分而不是等全部水合完成。这对大型页面很实用。五、Modern.js 与新生态了解 SSR 原理后我们来看看新一代框架是如何优化这个过程的。前段时间才发现字节 2021 年开源的 Modern.js 框架。可以列表对比一下这两个框架。技术Next.jsModern.js打包工具Webpack / TurbopackRspack (基于 Rust速度更快)转译工具Babel / SWCSWC 默认路由自研App Router / Pages RouterReact Router 7 约定式路由服务端框架自研Hono.js轻量高性能本人在构建了许多许多次 Express 框架以后才认识到 Express 确实比较老了但对于新手学习成本低生态也传统而稳定。我最近才了解到 Hono、Bun 等较新的生态更多构建工具相关的 Rsbuild、Rspack 等内容对我来说全是新的值得学一学拓展一下知识面。RspackWebpack 的 Rust 替代品API 兼容但构建速度快很多。解决大型项目 Webpack 构建慢的痛点。目前生态还不够成熟插件支持不如 Webpack。Hono轻量级 Web 框架跨运行时原生 Promise 支持。比 Express 更适合现代 JavaScript 生态。这些“新东西”解决了什么问题我发现一个规律从 Webpack 到 Rspack从 Express 到 Hono本质都是用编译型语言Rust/Go重写 JS 工具链。这不是卷也许也是哈哈哈是因为 JS 做构建工具已经到天花板了——单线程、动态类型、运行时开销大。Rspack 用 Rust 实现多线程并行编译在大型项目上构建速度能提升 5-10 倍。Hono 比 Express 强在哪跨运行时Node、Deno、Bun、Cloudflare Workers 都能跑原生 Promise 支持TypeScript 一等公民。Express 设计于 2010 年那时候还没有async/await异步中间件的处理方式现在看起来确实有点别扭。Modern.js 的定位它更像一个“全家桶”方案不仅仅是 SSR 框架还内置了 BFF 一体化、国际化、Monorepo 支持等。和 Next.js 的“轻核心 社区生态”走的是两条路。字节内部大规模验证过国内场景优化比如 Ant Design 按需加载做得更细致。六、小结回过头看SSR 和 CSR 没有绝对的好坏更多是场景驱动的选择。Next.js 让我“无感”恰恰说明它封装得足够好而 Express 虽老却依然适合新手入门。技术世界变化很快新生态、新框架、新概念总是日新月异。但好奇心永远是最好的导航。这些新东西不需要全都学但了解它们解决了什么问题或许能帮我们在合适的场景做出更好的选择。限于个人写作文中若有疏漏还请不吝赐教。参考文档React SSR 水合Hydration详解SEO - MDN Web 文档术语表Web 相关术语的定义 | MDNhydrateRoot – 反应 — hydrateRoot – React服务器端渲染 (SSR) | Vue.js 框架Getting Started: Server and Client Components | Next.js