# SPA 和 MPA

# SPA

  • 单页应用又称 SPA(Single Page Application)指的是使用单个 html 完成多个页面切换和功能的应用。这些应用只有一个 html 文件作为入口,一开始只需加载一次 js、css 等相关资源,然后使用 js 完成页面的布局和渲染,页面展示和功能是根据路由完成的

  • 单页应用跳转,就是切换相关组件,仅刷新局部资源

# MPA

  • 多页应用又称 MPA(Multi Page Application)指有多个独立的页面的应用,每个页面必须重复加载 js、css 等相关资源

  • 多页应用跳转,需要整页资源刷新

# SSR

# 什么是 SSR

服务端渲染是指将页面数据和页面模板组装成 HTML 的过程在服务端进行,客户端接收到完整的 HTML 内容,而不需要等待 JavaScript 执行完成才能看到页面内容

# Hydration 的作用

在 React 服务端渲染(Server Side Rendering,SSR)中,hydration 是一个关键步骤。

在服务器端渲染过程中,服务器会生成完整的 HTML 字符串并发送给浏览器。虽然用户可以立即看到页面内容,但此时的 HTML 是静态的,没有 JavaScript 交互能力。

hydration 就是让React在客户端 "接管" 这些已经存在的 HTML 元素,为它们添加事件监听器和状态管理,使页面变得可交互的过程。

# Hydration API 的演进

React 16-17 时期

从 React 16 开始,React 引入了 ReactDOM.hydrate (opens new window) 来专门处理 SSR 场景下的 hydration,区别于普通的 ReactDOM.render:

// 用于 SSR hydration
ReactDOM.hydrate(<App />, document.getElementById('root'));

// 用于客户端首次渲染
ReactDOM.render(<App />, document.getElementById('root'));
1
2
3
4
5

React 18 的重要变化

React 18 引入了全新的 API 和能力:

  1. 新的 Hydration API:hydrateRoot (opens new window)
// React 18 新 API
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);
1
2
3
  1. 并发特性支持 React 18 的 hydration 过程支持并发渲染,可以被中断和恢复,避免长时间阻塞主线程。

# Selective Hydration

React 18 引入了选择性 hydration 的重要特性:

核心机制:

  • 流式 SSR:HTML 可以边生成边发送给客户端,无需等待所有内容准备完毕

  • 优先级调度:当用户与某个组件交互时,React 会优先 hydrate 那个组件

  • Suspense 边界:不同的 Suspense 边界可以独立进行 hydration,互不阻塞

实际效果:

  • 页面可以更快地显示内容(HTML 流式传输)

  • 用户交互响应更及时(交互组件优先 hydrate)

  • 整体性能和用户体验显著提升

这意味着 React 18 的应用启动时不需要等待所有组件完成 hydration,而是可以逐步、智能地完成这个过程,同时保持良好的用户交互体验。

# 调和和水合的区别

  • 调和(Reconcile)是 React 等框架中的一个核心算法,用于比较新旧虚拟 DOM 树之间的差异,并确定需要对真实 DOM 进行哪些最小的更新操作。

  • 水合(Hydration)是 SSR 应用在客户端的一个特殊过程,它将服务端已经渲染好的静态 HTML 与客户端的 React 组件 "连接" 起来,使静态 HTML 变成可交互的动态应用。

# SSR 的好处

  • seo 优化,搜索引擎可以直接抓取到完整的 HTML 内容

  • 首屏性能,首屏性能决定 seo

# SSR 的原理

1. 项目构建(webpack、vite、rollup)

2. http 服务

3. ssr 应用本身(两大核心:数据预取和路由)

  • 数据预取

  • 路由

  • node 中间件: koa middleware

    • 日志收集

    • 301、302

  • jsx 渲染成 html 字符串

  • TDK

  • css

    • tailwindcss

    • postcss + css-modules

    • 不要用 css in js 方案,会有很多问题,比如 styled-components 会有性能瓶颈

    • 注意选好组件库,有的组件库是 css in js 的,也不能用,比如 Ant Design 5.0、mui (opens new window)建议用 shadcn (opens new window) 完全用 tailwindcss 写的

  • 水合

  • 做 ssr 不要用 nuxtjs,问题很多,nuxtjs2 完全不支持 serverless,nuxtjs3 虽然支持,但是也有很多问题

  • 传统 csr 要等 main.js 完全加载并执行完,才会去加载页面级的资源。但是 ssr 在一开始就会从服务端把 main.js 和页面级的资源都返回了

# 自定义 SSR 的应用场景

poc 版本,没有太多用户量,能用 nextjs 就用 nextjs,不满足或者有以下问题再用自定义 ssr:

  • 高并发,qps 达到几万甚至几十万

  • 解决冷启动问题,不用 nextjs,nextjs 冷启动 4-5s,每次部署,nextjs13+

    • 框架代码量大,几十万行

    • 为了更多通用场景,做了很多适配兼容

  • 每次请求响应时间 100-150ms

  • 大型复杂项目

    • 微服务(webpack5 模块联邦)

    • 分布式并发渲染:淘宝、虾皮

  • 高可用

  • csr 项目要改造成 ssr 项目

  • 业界使用自定义 ssr 的大厂业务:淘宝、钉钉、抖音电商、携程海外版 trip、虾皮、币圈前五的中心化交易所

什么是 POC 版本

POC 版本是指 "Proof of Concept"(概念验证)版本,是软件开发过程中的一个重要阶段。它帮助团队在投入大量资源之前,快速验证想法的可行性,是一种风险控制和决策支持工具。

POC 成功后,通常会进入MVP(最小可行产品)阶段,然后才是完整的产品开发。

需求分析 → POC 开发 → MVP 开发 → 正式版本开发

# 自定义 SSR 的注意事项

  • 自定义 ssr 的原理跟 nextjs 是不一样的,不论是数据预取还是路由。唯一一样的是都得使用 renderToPipeableStream (opens new window)

  • 自定义 ssr 的时候可以考虑增加降级为 csr 的逻辑,增加容错

  • 自定义 ssr 客户端需要自己起一个 HMR,不能直接用 webpack-dev-server 提供的

# 比较成熟的 SSR 架构

  • 使用【serverless 的边缘渲染 + 分布式渲染】去渲染 ssr 应用

  • 想要部署高并发(qps 至少七八千)的 ssr 项目,也得用 serverless 解决

# Webpack 构建服务端代码需要做的配置

  • externalsPresets: { node: true }

  • target: 'node' 客户端用的是 browserslist

  • 忽略掉 node_modules 中的三方依赖包,不进行打包,因为 serverless 服务端是允许有 node_modules 的

externals: [
	webpackNodeExternals: {}	     
]
1
2
3
  • 使用 ignore-loader 忽略掉 css、图片、字体

# Webpack 慢的优化方式

  • 使用文件系统缓存,filesystem

  • 将 babel-loader 换成 swc-loader,提升 30%

  • 使用 thread-loader

# 选择 Node 框架的时候要考虑性能

express 太重,建议用 koa 或者 fastify (opens new window)

# React 如何忽略水合报错

可以使用 suppressHydrationWarning (opens new window) 属性,不过不建议用。

# 同构开发

React 同构开发(Isomorphic JavaScript)是指编写一套可以同时在服务器端和客户端环境中运行的 JavaScript 代码的开发模式。在这种模式下,React 组件、路由配置、数据获取逻辑等可以在服务端用于生成 HTML,同时在客户端用于实现交互功能,从而实现代码复用和统一的开发体验。

# 优势

  • 提升⾸屏渲染速度,增强⽤户体验。

  • 对于搜索引擎优化(SEO)有益,因为搜索引擎爬⾍可以直接解析服务器返回的 HTML 内容。

# 注水脱水

React 同构渲染涉及到 “注⽔” 和 “脱⽔” 的概念,这两个术语来源于 React 服务器端渲染(SSR)的两个重要步骤。

  • 脱⽔(Dehydration):在服务器端,React 将组件树渲染为静态的 HTML 字符串,并⽣成与应⽤状态相关的数据(即 "脱⽔" 数据)。这个过程就被称为 “脱⽔”。然后,服务器将渲染后的 HTML 和脱⽔数据⼀起发送到客户端。

  • 注⽔(Hydration):在客户端,React 使⽤服务端发送过来的脱⽔数据来恢复应⽤的状态,这个过程被称为 “注⽔”。然后 React 会将服务器渲染的 HTML 和客户端组件树进⾏匹配,如果匹配成功,React 将接管这些已经存在的 DOM 节点,使其变得可以交互。

// === 服务端脱水 ===
function serverRender() {
  const initialState = { count: 0 };
  const html = renderToString(<Counter initialCount={0} />);
  
  return `
    <div id="root">${html}</div>
    <script>
      window.__STATE__ = ${JSON.stringify(initialState)};
    </script>
  `;
}

// === 客户端注水 ===
function clientHydrate() {
  const state = window.__STATE__;
  hydrateRoot(
    document.getElementById('root'),
    <Counter initialCount={state.count} />
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

注意

水合和注水都是 Hydration,它们是一模一样的过程,只是叫法不一样而已。

这种同构渲染的⽅式有⼏个好处:

  • ⾸先,⽤户可以更早地看到⻚⾯内容,因为浏览器⽆需等待所有的 JavaScript 代码下载和执⾏完毕就可以显示服务器渲染的 HTML;

  • 其次,由于服务器已经⽣成了初始状态,客户端⽆需再次获取数据;

  • 最后,React 可以复⽤服务器渲染的 DOM,避免了额外的 DOM 操作,从⽽提⾼了性能。

但需要注意,这种⽅式也有⼀些缺点,例如服务器端渲染可能会增加服务器的负载,以及在注⽔过程中可能会出现⼀些问题,如数据不⼀致或者错误的状态等。

# 部署方式

  • 使⽤ Next.js:Next.js 是⼀个流⾏的、基于 React 的通⽤ JavaScript 框架。它处理了很多同构开发中的常⻅问题,包括路由、数据预取和预渲染等。使⽤ Next.js 通常可以节省⼤量开发时间,并且提供了⼀个成熟稳定的平台。

  • ⾃⼰开发:这需要使⽤ Node.js 服务器(如 Express)来预渲染 React 应⽤,然后发送⽣成的 HTML 到浏览器。这种⽅式需要解决很多细节问题,例如:代码分割、数据预取、路由匹配等。

  • 云平台选择:主要有 AWS 和 Cloudflare。

在部署同构应⽤时,你需要在服务器上部署 Node.js 环境,并在其中运⾏你的 Next.js 或⾃⼰开发的应⽤。你还需要配置适当的反向代理(如 Nginx),以便将请求转发到 Node.js 服务器。

在实际落地过程中,可能会遇到以下⼀些挑战:

  • 数据预取:在服务器端,我们需要在渲染⻚⾯之前预取数据。常⻅的解决⽅案包括在 React 组件中使⽤静态的 loadData ⽅法,或者使⽤像 Next.js 这样的框架。

  • 组件的⽣命周期⽅法:只有部分⽣命周期⽅法会在服务器端渲染过程中被调⽤,例如 componentDidMount 和 componentDidUpdate 就不会被调⽤。因此需要确保应⽤的逻辑不依赖这些只在客户端执⾏的⽣命周期⽅法。

  • 性能和资源使⽤:服务器端渲染会占⽤更多的服务器资源(CPU 和内存)。需要通过适当的架构和优化来保证服务器的性能,例如使⽤流式渲染、设置合理的缓存策略等。

# 性能指标

同构渲染⼀般会对以下性能指标产⽣影响:

  • ⾸次内容绘制(FCP,First Contentful Paint):SSR 可以减少 FCP 时间,因为浏览器可以在接收到服务器返回的 HTML 后⽴即开始渲染⻚⾯。

  • ⾸次有意义的绘制(FMP,First Meaningful Paint):SSR 也可能减少 FMP 时间,因为服务器预渲染的⻚⾯通常包含了⻚⾯的主要内容。

不过 FMP 已经从 Web 性能指标中移除了。现在 LCP、INPCLS核心 Web 指标 (opens new window),它们是 Google 推荐所有⽹站关注的最重要的性能指标。

# 流式渲染

流式渲染是指服务器⽣成 HTML 的过程可以以数据流(stream)的形式逐步发送到客户端,⽽不是等待全部内容⽣成完成后⼀次性发送。

// 传统 SSR:等待所有内容准备完毕
const fullHTML = await renderToString(<App />);
res.send(fullHTML); // 一次性发送

// 流式渲染:边生成边发送
const stream = renderToNodeStream(<App />);
stream.pipe(res); // 流式发送
1
2
3
4
5
6
7

这样做的优势主要有两点:

  • 提升⾸屏渲染速度:浏览器可以更早地开始解析和渲染⻚⾯,提⾼⽤户体验。

  • 减少服务器内存使⽤:由于服务器不需要为每个请求保存完整的 HTML 字符串,因此可以显著减少服务器的内存使⽤。

# 各种渲染方式优缺点

# CSR

客户端渲染就是指将页面数据和页面模板组装成 html 的过程在客户端进行,服务器直接转发静态 html 资源即可

打包的时候生成只有 css、js 等外链标签和根节点的 html 页面,客户端在请求时,服务端不做任何处理,直接以原文件的形式返回给客户端,客户端获取到页面后,等加载完 js 后才通过 js 来渲染页面内容。

优点:

  • 交互体验流畅,切换页面无刷新

  • 服务器压力小,只需提供静态资源

  • 前后端分离,开发效率高

缺点:

  • 首屏加载慢,需等待 JS 执行

  • SEO 不友好,搜索引擎难以抓取内容

  • 白屏时间长,用户体验较差

适用场景:

后台管理系统、对 SEO 要求不高的应用

# 预渲染

预渲染就是指构建阶段预先生成匹配预渲染路径的 html 文件,每个需要预渲染的路由都有一个对应的 html,生成的 html 文件已有内容

如果项目中有使用 webpack,可以使用 prerender-spa-plugin (opens new window) 轻松添加预渲染。

优点:

  • 首屏加载快,HTML 已预先生成

  • SEO 友好,内容完整可抓取

  • 服务器压力小,只需提供静态文件

  • 可部署到 CDN,全球加速

缺点:

  • 构建时间长,每次更新需重新构建

  • 不适合动态内容,内容更新不及时

  • 路由需在构建时确定

适用场景:

博客、文档网站、官网、营销页面

# SSR

服务端渲染就是指将页面数据和页面模板组装成 html 的过程在服务端进行,客户端不需要渲染页面

服务端渲染模式下,当用户第一次请求页面时,由服务器把需要的组件或页面渲染成 html 字符串,然后把它返回给客户端。客户端拿到手的,是可以直接渲染然后呈现给用户的 html 内容,不需要为了生成 dom 内容自己再去跑一遍 js 代码。使用服务端渲染的网站,可以说是 “所见即所得”,页面上呈现的内容,我们在 html 源文件里也能找到。

优点:

  • 首屏加载快,用户立即看到内容

  • SEO 友好,搜索引擎可直接抓取 HTML

  • 更好的用户体验和性能指标

缺点:

  • 服务器压力大,每次请求都需渲染

  • 开发复杂度高,需要考虑前后端环境差异

  • TTFB(首字节时间)可能较长

适用场景:

电商网站、新闻网站、需要SEO的营销页面

# 同构渲染

同构渲染就是指在服务端先进行渲染(SSR,生成完整的 HTML 内容),客户端接收到 HTML 后,再进行 hydration 过程

Hydration 是指 React 在客户端 "接管" 服务端渲染的静态 HTML,为其添加事件监听器、恢复组件状态,使页面变得可交互。在这个过程中,React 会对比服务端渲染的内容与客户端期望的内容,如果发现不一致,会在开发环境给出警告,并尝试修复差异,最终让页面具备完整的 CSR 交互能力。在 React 18 中,hydration 过程支持并发渲染和选择性激活,可以优先处理用户正在交互的组件。

为了同时拥有 ssr 和 csr 的特点,当前流行的方案就是 ssr + csr 同构,比如现在比较流行的的 Next.js (opens new window)

优点:

  • 结合 SSR 和 CSR 的优势

  • 首屏快速加载 + 后续流畅交互

  • SEO 友好且用户体验佳

  • 支持流式渲染和选择性激活

缺点:

  • 开发复杂度最高

  • 需要处理 hydration 问题

  • 服务器和客户端都有性能开销

  • 调试和维护难度大

适用场景:

大型电商平台、内容与交互并重的应用、现代 Web 应用的主流选择

上次更新时间: 2025年09月24日 00:11:17