SPA 和 MPA
# SPA 和 MPA
# SPA
单页应用又称 SPA(Single Page Application)指的是使用单个 html 完成多个页面切换和功能的应用。这些应用只有一个 html 文件作为入口,一开始只需加载一次 js、css 等相关资源,然后使用 js 完成页面的布局和渲染,页面展示和功能是根据路由完成的。
单页应用跳转,就是切换相关组件,仅刷新局部资源。
# MPA
多页应用又称 MPA(Multi Page Application)指有多个独立的页面的应用,每个页面必须重复加载 js、css 等相关资源。
多页应用跳转,需要整页资源刷新。
# SSR
一个好的架构需要考虑到高性能、高可用、扩展性、安全性这几方面。
# 什么是 SSR
服务端渲染是指将页面数据和页面模板组装成 HTML 的过程在服务端进行,客户端接收到完整的 HTML 内容,而不需要等待 JavaScript 执行完成才能看到页面内容。
# 怎么查看是不是 SSR
右键 → 查看网页源代码(Ctrl+U / Cmd+U),如果源码里能看到完整的 HTML 内容(文字、列表、文章正文等),就是 SSR。如果源码里只有一个空的 <div id="app"></div> 或 <div id="root"></div>,就是 CSR。
# 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 变成可交互的动态应用。
# 如何忽略水合报错
可以使用 suppressHydrationWarning (opens new window) 属性,不过不建议用。
# SSR 的目的
SEO + AIO 优化,搜索引擎可以直接抓取到完整的 HTML 内容
提升首屏性能,首屏性能决定 SEO,核心性能指标:LCP (opens new window)、INP (opens new window)、CLS (opens new window)、FCP (opens new window)、TTFB (opens new window)
AIO、AI 爬虫和 Sitemap
AIO(AI Optimization)是 SEO(搜索引擎优化)在 AI 时代的进化版。传统 SEO 的目标是让你的网站在 Google 搜索结果里排名靠前;AIO 的目标是让你的内容被 AI(ChatGPT、Claude、Perplexity 等)引用和推荐。SEO 让 Google 找到你,AIO 让 AI 提到你。
ClaudeBot (opens new window) 和 GPTBot (opens new window) 分别是 Anthropic 和 OpenAI 的爬虫,它们抓取的内容会分别喂给各自的模型 Claude 和 ChatGPT,这样用户在向它们问问题时就会引用你的内容进行回答,那么你的 AIO 就达成了目标。
GPTBot、ClaudeBot 等 AI 爬虫不执行 JavaScript,只能读取初始 HTML。用 React/Vue/Angular 客户端渲染的内容对这些爬虫来说是不可见的。因此,使用 SSR 也有利于这些爬虫抓取内容。
- Sitemap(站点地图) (opens new window)在 AIO 里扮演两个角色:
① 传统 sitemap.xml
一份包含 lastmod 日期的有效 sitemap.xml,帮助 AI 爬虫发现并优先抓取你的内容。
② 新标准 llms.txt
llms.txt (opens new window) 是一个新兴的社区标准,相当于专门为 AI 系统设计的 sitemap,提供对网站用途的高度浓缩的摘要,并指向最重要的文档页面。许多 AIO 从业者认为,llms.txt 最终将在 AI 搜索中扮演类似 sitemap.xml 在传统 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 和 Node.js 框架推荐
1. SSR 框架
Nextjs (opens new window):最成熟、生态最完善,不过与 Vercel 绑定较深,App Router 学习曲线陡,项目越大构建越慢,还有就是近几年安全漏洞太多,不太推荐使用。
React Router (opens new window):React Router v7 开始正式支持 SSR,同时也是 Remix 的继承者(Remix 已并入 React Router),路由即框架,迁移成本低,轻量灵活,不过生态没有 Nextjs 完善,目前首选推荐使用。
Tanstack Start (opens new window):TanStack 出品(就是做 React Query、React Table 的那个团队),基于 TanStack Router,类型安全极强,与 TanStack Query 集成天然,理念现代,目前处于 v0 RC 阶段,功能已基本稳定,API 趋于固定,但还未发布 v1 正式版,社区和文档还不完善。
Astro (opens new window):多框架支持(React/Vue/Svelte 混用),主打 "岛屿架构",默认输出静态 HTML,只在需要的地方注水。默认零 JS,性能极好,内容型网站首选,框架无关;交互复杂的应用不适合,动态数据场景下配置繁琐。
Waku (opens new window):Zustand、Jotai 作者做的极简 React SSR 框架,专注 React Server Components。极度轻量,RSC 支持好,适合研究 RSC 原理;非常早期,生产环境几乎没人用,功能不完整。
Nuxtjs (opens new window):Vue 生态首选,和 Vue 的契合度极高,Nitro 引擎部署灵活,开发体验很好,约定优于配置,上手快,模块生态丰富。不过框架本身封装较深,遇到边缘问题排查成本高,国际上影响力仍不及 Next.js。
2. Node.js 框架
Koa (opens new window):轻量灵活,本身只提供中间件机制,其他功能全靠插件组合,适合自定义程度高的项目。用 async/await 替代了 Express 的回调写法,洋葱模型中间件是它最标志性的设计。
Fastify (opens new window):主打高性能的 Node.js 框架,JSON Schema 验证和序列化是内置的,路由性能在 Node.js 框架里数一数二。TypeScript 支持友好,插件体系也比较完善。
Hono (opens new window):新生代框架,最初为 Cloudflare Workers 设计,目标是 "运行在任何 JS 运行时" —— Node.js、Deno、Bun、Edge 环境全支持,因此对 serverless 非常友好。极度轻量,性能很强。
如果你的 Serverless 场景就是跑在标准 Node.js 上(比如 AWS Lambda 的 Node runtime),Koa 和 Fastify 都可以。不过如果没有特别理由建议优先选 Fastify,因为 Serverless 环境下冷启动和响应速度很关键,Fastify 在这两点上都明显优于 Koa:
性能:Fastify 的路由和 JSON 序列化经过专门优化,吞吐量比 Koa 高不少;
包体积:两者体积都不大,这点差距不明显;
内置能力:Fastify 内置了参数校验、序列化,Koa 需要额外装插件,Serverless 里插件越多包越大。
如果目标平台是 Edge / Cloudflare Workers 这类非 Node.js 环境,Hono 是目前最友好的选择,没有之一。
# 什么时候需要自定义 SSR
如果是 poc 版本,没有太多用户量,能用 nextjs 就用 nextjs。
什么是 POC 版本
POC 版本是指 "Proof of Concept"(概念验证)版本,是软件开发过程中的一个重要阶段。它帮助团队在投入大量资源之前,快速验证想法的可行性,是一种风险控制和决策支持工具。
POC 成功后,通常会进入MVP(最小可行产品)阶段,然后才是完整的产品开发。
需求分析 → POC 开发 → MVP 开发 → 正式版本开发
如果有以下问题或者考量再使用自定义 ssr:
性能问题:nextjs 冷启动慢,只要项目代码稍微多一点,就得 5000ms,自定义 ssr 可以做到 800ms,nextjs 每个请求渲染时间 100ms,自定义 ssr 可以做到 50ms;
安全性:近几年 nextjs 已经出现了好几个高危漏洞;
稳定性(高可用):需要考虑服务降级,比如出问题时降级到 csr,有一定的容错,以及熔断;
扩展性:nextjs 有些功能是 vercel 独享,安全属性 SRI 和 nonce,middleware 上下文不全;
老项目迁移:比如 csr 项目要改造成 ssr 项目,使用 nextjs 的话迁移成本和风险都会很大;
业界使用自定义 ssr 的大厂业务有:淘宝、钉钉、抖音电商、携程海外版 trip、虾皮、币圈前五的中心化交易所
什么是熔断
熔断(Circuit Breaker)是微服务/分布式系统中的一种容错模式,灵感来自电路中的保险丝:当下游服务出现故障或响应超时时,自动 "断开" 对该服务的调用,防止故障扩散、保护整体系统稳定性。
SSR 熔断是指在服务端渲染链路中,当依赖的接口出现异常时,自动 "断路" 并返回降级内容,避免一个接口拖垮整个渲染服务。它的核心价值如下:
| 对比维度 | ❌ 没有熔断 | ✅ 有熔断 |
|---|---|---|
| 下游接口变慢 | SSR 服务全面崩溃 | 自动降级,返回兜底页,SSR 继续存活 |
| 用户体验 | 白屏或 500 错误页 | 骨架屏或缓存内容,体验可接受 |
| 故障传播 | 单点故障扩散至整个服务 | 故障被隔离,仅局部降级 |
| 系统恢复 | 需人工介入重启 | 半开状态自动探测,服务自愈 |
| 资源消耗 | 线程堆积,内存溢出(OOM) | 快速失败,释放线程资源 |
| 可观测性 | 故障难以定位,链路不清晰 | 熔断状态可监控,故障边界清晰 |
专门做熔断的库有:Opossum (opens new window)(Node.js)、Hystrix (opens new window)(Java,已停止维护)、Resilience4j (opens new window)(Java,可替代 Hystrix)
# 自定义 SSR 的注意事项
自定义 ssr 的原理跟 nextjs 是不一样的,不论是数据预取还是路由。唯一一样的是都得使用 renderToPipeableStream (opens new window)
自定义 ssr 的时候可以考虑增加降级为 csr 的逻辑,增加容错
自定义 ssr 客户端需要自己起一个 HMR,不能直接用 webpack-dev-server 提供的
# 比较成熟的 SSR 架构
使用【serverless 的边缘渲染 + 分布式渲染】去渲染 ssr 应用
想要部署高并发(qps 至少七八千)的 ssr 项目,也得用 serverless 解决
# 分布式渲染和分布式并发渲染
- 分布式渲染(Distributed Rendering)是指将页面的不同部分分散到多个服务器或服务上进行渲染,然后组装成完整页面的技术。
// 主服务器组装页面
app.get('/page', async (req, res) => {
const [header, content, sidebar, footer] = await Promise.all([
fetch('http://header-service/render'), // 服务 A
fetch('http://content-service/render'), // 服务 B
fetch('http://sidebar-service/render'), // 服务 C
fetch('http://footer-service/render') // 服务 D
]);
const html = `
<html>
<body>
${await header.text()}
<div class="main">
${await content.text()}
${await sidebar.text()}
</div>
${await footer.text()}
</body>
</html>
`;
res.send(html);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 在 CDN 边缘节点分布式渲染
const edgeFunction = async (request) => {
const location = request.cf.country;
// 根据地理位置选择最近的渲染服务
const renderService = selectNearestService(location);
return await renderService.render(request);
};
2
3
4
5
6
7
8
9
- 分布式并发渲染(Distributed Concurrent Rendering)是指在分布式渲染的基础上,利用并发机制同时处理多个渲染任务,提高整体渲染效率。
// 使用 React 18 的并发特性进行分布式渲染
import { renderToPipeableStream } from 'react-dom/server';
const DistributedPage = () => {
return (
<div>
<Suspense fallback={<HeaderSkeleton />}>
<RemoteHeader serviceUrl="http://header-service" />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<RemoteContent serviceUrl="http://content-service" />
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<RemoteSidebar serviceUrl="http://sidebar-service" />
</Suspense>
</div>
);
};
// 流式渲染 + 并发处理
const stream = renderToPipeableStream(<DistributedPage />, {
onShellReady() {
// 页面骨架立即发送
stream.pipe(response);
},
onAllReady() {
// 所有组件渲染完成
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 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
# 同构开发
# 什么是同构开发
React 同构开发(Isomorphic JavaScript)是指编写一套可以同时在服务器端和客户端环境中运行的 JavaScript 代码的开发模式。在这种模式下,React 组件、路由配置、数据获取逻辑等可以在服务端用于生成 HTML,同时在客户端用于实现交互功能,从而实现代码复用和统一的开发体验。
# 同构开发的好处
同构开发的最大好处是 "一套代码,双重收益":既获得了服务端渲染的 SEO 和性能优势,又保持了客户端应用的交互体验,同时大幅降低了开发和维护成本。
# React 同构 API
服务端渲染 API
// React 18 - 流式渲染(推荐)
import { renderToPipeableStream } from 'react-dom/server';
const stream = renderToPipeableStream(<App />, {
onShellReady() {
response.setHeader('Content-type', 'text/html');
stream.pipe(response);
}
});
// React 17/18 - 传统方式
import { renderToString } from 'react-dom/server';
const html = renderToString(<App />);
2
3
4
5
6
7
8
9
10
11
12
客户端激活 API
// React 18
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);
// React 17
import { hydrate } from 'react-dom';
hydrate(<App />, document.getElementById('root'));
2
3
4
5
6
7
完整的 React 同构示例
// server.js
import { renderToPipeableStream } from 'react-dom/server';
app.get('*', (req, res) => {
const stream = renderToPipeableStream(
<StaticRouter location={req.url}>
<App />
</StaticRouter>
);
stream.pipe(res);
});
// client.js
import { hydrateRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
hydrateRoot(
document.getElementById('root'),
<BrowserRouter>
<App />
</BrowserRouter>
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
也可以使用 Next.js (opens new window) 进行同构开发。
# Vue 同构 API
服务端渲染 API
// Vue 3
import { renderToString } from '@vue/server-renderer';
import { createSSRApp } from 'vue';
const app = createSSRApp({
template: `<div>{{ message }}</div>`,
data: () => ({ message: 'Hello from SSR!' })
});
const html = await renderToString(app);
// Vue 3 流式渲染
import { renderToNodeStream } from '@vue/server-renderer';
const stream = renderToNodeStream(app);
2
3
4
5
6
7
8
9
10
11
12
13
14
客户端激活 API
// Vue 3
import { createSSRApp } from 'vue';
const app = createSSRApp({
// 同样的应用定义
});
app.mount('#app', true); // 第二个参数 true 表示 hydrate
2
3
4
5
6
7
8
完整的 Vue 同构示例
// server.js
import { createSSRApp } from 'vue';
import { renderToString } from '@vue/server-renderer';
import { createMemoryHistory, createRouter } from 'vue-router';
export async function createApp() {
const app = createSSRApp(App);
const router = createRouter({
history: createMemoryHistory(),
routes
});
app.use(router);
await router.push(url);
await router.isReady();
return { app, router };
}
// 服务端
const { app } = await createApp();
const html = await renderToString(app);
// client.js
import { createSSRApp } from 'vue';
import { createWebHistory, createRouter } from 'vue-router';
const app = createSSRApp(App);
const router = createRouter({
history: createWebHistory(),
routes
});
app.use(router);
router.isReady().then(() => {
app.mount('#app', true);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
也可以使用 Nuxt.js (opens new window) 进行同构开发。
# 注水脱水
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 推荐所有⽹站关注的最重要的性能指标。
# 流式渲染
流式渲染 (opens new window)是指服务器⽣成 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 字符串,因此可以显著减少服务器的内存使⽤。
# 各种渲染方式优缺点
先按主要 HTML 在什么时候生成来区分这四种方式:
| 方式 | 主要 HTML 的生成时机 | 适合场景 | 需要承担的代价 |
|---|---|---|---|
| CSR(Client-Side Rendering,客户端渲染) | 浏览器下载并执行 JavaScript 后 | 后台系统、登录后的强交互页面 | 首屏依赖 JavaScript,SEO 较弱 |
| SSR(Server-Side Rendering,服务端渲染) | 每次请求到达服务器时 | 个性化、变化频繁且需要首屏 HTML 的页面 | 每次请求都要计算,缓存和运行成本更高 |
| SSG(Static Site Generation,静态站点生成) | 构建项目时 | 博客、文档、官网等内容稳定的公开页 | 内容变化后通常需要重新生成 |
| ISR(Incremental Static Regeneration,增量静态再生成) | 先返回缓存的静态页面,过期或失效后再生成新版本 | 内容需要更新,但不要求每次请求都实时计算的公开页 | 可能短暂返回旧内容,缓存失效策略更复杂 |
不要把两组概念混在一起
CSR、SSR、SSG 和 ISR 回答的是主要 HTML 在什么时候生成;Server Component 和 Client Component 回答的是组件代码主要在哪一侧执行、哪些代码需要发送到浏览器。
它们不是互斥的一组概念。例如 Next.js 页面可以在请求时使用 SSR 生成 HTML,同时由 Server Component 获取数据,并在页面中嵌入负责点击、拖拽和浏览器 API 的 Client Component。Server Component 与 Client Component 的详细用法可以继续查看 React Server Components。
# CSR
客户端渲染就是指将页面数据和页面模板组装成 html 的过程在客户端进行,服务器直接转发静态 html 资源即可。
打包的时候生成只有 css、js 等外链标签和根节点的 html 页面,客户端在请求时,服务端不做任何处理,直接以原文件的形式返回给客户端,客户端获取到页面后,等加载完 js 后才通过 js 来渲染页面内容。
优点:
交互体验流畅,切换页面无刷新
服务器压力小,只需提供静态资源
前后端分离,开发效率高
缺点:
首屏加载慢,需等待 JS 执行
SEO 不友好,搜索引擎难以抓取内容
白屏时间长,用户体验较差
适用场景:
后台管理系统、对 SEO 要求不高的应用
# SSG(静态站点生成)
SSG(Static Site Generation,静态站点生成)也常被称为静态预渲染,是指在构建阶段预先生成各个路由对应的 HTML 文件,用户访问时直接返回已经生成好的页面。
如果项目中有使用 webpack,可以使用 prerender-spa-plugin (opens new window) 轻松添加预渲染。
优点:
首屏加载快,HTML 已预先生成
SEO 友好,内容完整可抓取
服务器压力小,只需提供静态文件
可部署到 CDN,全球加速
缺点:
构建时间长,每次更新需重新构建
不适合动态内容,内容更新不及时
路由需在构建时确定
适用场景:
博客、文档网站、官网、营销页面
# ISR(增量静态再生成)
ISR(Incremental Static Regeneration,增量静态再生成) (opens new window)可以理解为允许静态页面在网站运行期间按规则更新。它保留了 SSG 直接返回缓存页面的速度,又不需要每次内容变化都重新构建整个网站。
一次典型的定时更新过程如下:
- 构建项目时先生成静态页面并放入缓存。
- 在设定的有效期内,请求直接获得缓存页面。
- 有效期结束后的下一次请求仍可以先拿到旧页面,同时服务器在后台生成新版本。
- 新版本生成成功后替换旧缓存,后续请求获得新页面;生成失败时继续保留上一次成功版本。
除了按照时间更新,也可以在文章发布、商品修改等事件发生后,使用 revalidatePath 或 revalidateTag 主动让相关缓存失效。
优点:
大多数请求直接读取静态缓存,响应速度快,服务器压力低
更新部分内容时不需要重新构建整个网站
可以在大量内容页面和构建时间之间取得平衡
缺点:
缓存更新完成前可能暂时返回旧内容,不适合余额、订单等强实时数据
需要设计更新时间、按需失效和多实例缓存同步策略
它仍需要能够执行再生成逻辑的运行环境,不等于纯静态文件导出
适用场景:
资讯、博客、商品详情和内容目录等公开页面:内容会更新,但不要求每一次请求都读取最新数据。
可以把三者记成一句话:SSG 是构建时生成一次,ISR 是静态页面运行期间按规则更新,SSR 是每次请求时重新生成。
# 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 应用的主流选择