# 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'));
2
3
4
5
React 18 的重要变化
React 18 引入了全新的 API 和能力:
- 新的 Hydration API:hydrateRoot (opens new window)
// React 18 新 API
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);
2
3
- 并发特性支持 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)
服务端代码 nodejs
HMR、nodemon、serverless-http
@loadable/server (opens new window) 分析处理服务端 css、js 资源,根据不同的路由返回不同的资源
客户端代码
HMR、webpack-dev-server、webpack-hot-middleware
2. http 服务
3. ssr 应用本身(两大核心:数据预取和路由)
数据预取
csr、ssr:react query
数据预取的其他方式:nextjs 的 getServerSideProps (opens new window)
路由
react-router-dom
tankstack router (opens new window)(新出,可以尝鲜)
首次访问页面的时候是请求服务端,后续页面跳转都是客户端路由
如果是动态路由,切换路由的时候才回去加载资源;如果是静态路由,首次访问的时候就会一起加载
客户端路由 BrowserRouter (opens new window),服务端路由 StaticRouter (opens new window)
node 中间件: koa middleware
日志收集
301、302
jsx 渲染成 html 字符串
TDK
TDK 是网页 SEO(搜索引擎优化)中的三个核心元素:Title(标题)、Description(描述)、Keywords(关键词)
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 写的
水合
- 使用 react-dom/client 的 hydrateRoot (opens new window)
做 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
容错
熔断 hystrix (opens new window)、opossum (opens new window)(专门做熔断服务的库)
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: {}
]
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} />
);
}
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、INP 和 CLS 是核心 Web 指标 (opens new window),它们是 Google 推荐所有⽹站关注的最重要的性能指标。
# 流式渲染
流式渲染是指服务器⽣成 HTML 的过程可以以数据流(stream)的形式逐步发送到客户端,⽽不是等待全部内容⽣成完成后⼀次性发送。
// 传统 SSR:等待所有内容准备完毕
const fullHTML = await renderToString(<App />);
res.send(fullHTML); // 一次性发送
// 流式渲染:边生成边发送
const stream = renderToNodeStream(<App />);
stream.pipe(res); // 流式发送
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 应用的主流选择