Note 7
前端打包对于图片资源是否需要进行 gzip/br 压缩
通常情况下,不需要也不应该对已经压缩过的图片格式(如 JPEG, PNG, WebP, AVIF)进行 Gzip 或 Brotli 压缩。
为什么不需要?主要原因有三点:
收益极低,甚至可能适得其反
- 工作原理:
Gzip和Brotli这类压缩算法主要针对文本内容进行优化。它们通过查找和替换重复的字符串模式来减小文件大小。 - 图片的本质:像
JPEG、PNG这类常见的图片格式,其本身就是已经压缩过的二进制格式。它们使用了各自的压缩算法(如JPEG的有损压缩,PNG的无损压缩)来尽可能减少文件大小。图片文件中的数据(尤其是照片类JPEG)已经非常随机,几乎没有可供Gzip/Brotli再次压缩的重复模式。 - 结果:试图对它们进行二次压缩,压缩率会非常低(通常只有
2%-5%的缩小),有时甚至因为压缩文件头等元数据的增加,导致压缩后的文件比原文件更大。这个过程浪费了服务器CPU资源,却几乎没有换来任何带宽节省。
- 工作原理:
浪费服务器和客户端资源
- 服务器 CPU:服务器需要对每个请求的图片资源进行实时压缩(如果配置了动态压缩),或者需要预先存储压缩后的版本。这对于大量图片的网站来说,是一笔不小的
CPU开销。 - 客户端 CPU:浏览器在下载完成后,需要解压这些资源才能渲染。虽然现代设备解压很快,但这个不必要的步骤仍然会消耗设备电量,并在极端情况下可能略微延迟图片的渲染时间。
- 服务器 CPU:服务器需要对每个请求的图片资源进行实时压缩(如果配置了动态压缩),或者需要预先存储压缩后的版本。这对于大量图片的网站来说,是一笔不小的
现代图片格式已经足够优秀
- 现在主流的现代图片格式,如 WebP 和 AVIF,相比传统的
JPEG和PNG,在同等质量下拥有更小的文件体积。将精力放在将图片转换为现代格式上,其收益远远大于对旧格式进行Gzip/Brotli压缩。
- 现在主流的现代图片格式,如 WebP 和 AVIF,相比传统的
什么时候需要对“图片”进行 Gzip/Brotli 压缩?
有一个重要的例外情况:对 SVG 格式的图片进行压缩。
为什么 SVG 需要压缩?
- SVG 的本质:
SVG(Scalable Vector Graphics)文件本身是 XML 格式的文本文件。它里面包含的是标签、路径数据、样式等文本信息,这些信息有很多重复的模式(例如大量的<path>标签、重复的样式定义等)。 - 高压缩率:正因为它是文本,所以
Gzip或Brotli对其压缩效果非常显著,通常可以减少 50% - 80% 的体积。这对于复杂的SVG图标或插图来说是巨大的性能提升。
最佳实践建议:
在
Nginx或Apache等服务器的配置中,确保将SVG文件包含在需要压缩的MIME类型中。例如在
Nginx中:nginxgzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json image/svg+xml; # 确保包含这一行!
总结与最佳实践
| 资源类型 | 推荐使用 Gzip/Brotli | 原因 |
|---|---|---|
| HTML, CSS, JS, JSON | 强烈推荐 | 文本文件,包含大量重复模式,压缩效果极佳。 |
| SVG 图片 | 强烈推荐 | 本质是 XML 文本,压缩率非常高。 |
| JPEG, PNG, GIF | 不推荐 | 已是压缩过的二进制格式,二次压缩收益极低,浪费资源。 |
| WebP, AVIF | 不推荐 | 采用更先进的压缩算法,已是压缩后的最佳状态。 |
所以,对于前端项目打包后的图片资源,你应该做的是:
- 格式优化:将项目中的
JPEG、PNG图片转换为 WebP 格式。现在浏览器支持度已经非常广,能带来显著的体积减小。 - 质量调整:根据使用场景(如大图展示 vs. 小图标),适当降低图片的输出质量。
- 尺寸适配:根据不同的设备和屏幕尺寸,输出不同尺寸的图片(响应式图片,使用
srcset和sizes属性)。 - 压缩 SVG:确保你的构建流程或服务器对
SVG文件启用了Brotli(优先)或Gzip压缩。 - CDN 与缓存:使用
CDN并配置正确的缓存策略,这比压缩图片本身带来的性能提升可能更大。
通过以上这些方法,你可以更高效地优化前端项目中的图片资源,而不是在效果甚微的 Gzip/Brotli 压缩上浪费时间。
静态资源是否需要全部放到 CDN 上?
静态资源不需要,也不应该“全部”放到CDN上。 这是一个需要根据资源类型、业务需求、成本和架构进行权衡的决策。
为什么需要把大部分静态资源放到CDN上?(放CDN的好处)
将主要的、公共的静态资源(如JS、CSS、图片、字体、视频)放到CDN上,主要是为了:
极致性能,降低延迟
- 就近访问:
CDN在全球分布有多个边缘节点,用户可以从地理上最近的节点获取资源,极大减少网络传输的延迟。 - 缓存命中:热门资源会被缓存在各个边缘节点,用户请求无需回源,直接命中缓存,速度极快。
- 就近访问:
减轻源站服务器负载
- 静态资源请求占据了网站流量的绝大部分。将这些请求分流到
CDN,可以显著减少源站服务器的带宽、CPU和连接数压力,让源站更专注于处理动态API请求。
- 静态资源请求占据了网站流量的绝大部分。将这些请求分流到
提升可用性和容灾能力
- 即使源站服务器因故障暂时宕机,用户仍然可以从
CDN缓存中获取已缓存的静态资源,网站至少可以部分访问(虽然动态功能会失效)。 CDN本身具备高可用和负载均衡能力,能抵御一定程度的流量冲击(如热点事件、DDoS攻击的一部分)。
- 即使源站服务器因故障暂时宕机,用户仍然可以从
优化带宽成本
CDN厂商的带宽单价通常比你自己购买云服务器带宽要便宜。对于流量巨大的网站,使用CDN能节省可观的带宽费用。
不放CDN时有什么问题?
如果不使用CDN,所有静态资源都直接从你的源站服务器提供,会面临:
- 用户体验差:对于远离你机房的用户,加载速度会明显变慢,首屏时间变长。
- 服务器压力大:所有静态资源请求都直接打到源站,容易在流量高峰时成为性能瓶颈,导致服务器响应变慢甚至崩溃。
- 单点故障风险:源站成为唯一的出入口,一旦出现问题,整个网站完全无法访问。
- 成本可能更高:为了应对峰值流量,你可能需要购买更高配置的服务器和更昂贵的带宽,但大部分时间这些资源是闲置的。
不放CDN时,同一域名下更容易出现并发请求限制问题,从而影响页面加载性能。
浏览器并发请求限制机制
浏览器为了防止对单个服务器的过度请求和避免网络拥堵,对同一域名的并发HTTP/1.1请求数量有严格限制。
- HTTP/1.1:现代浏览器对同一域名的并发请求数通常是 6个。这意味着,如果页面需要加载超过
6个来自同一个域名的资源,第7个及以后的请求必须排队等待,直到前面的请求完成一个,空出一个“位置”。 - HTTP/2:这个限制被大大缓解了。
HTTP/2支持多路复用,可以在同一个TCP连接上同时进行多个请求和响应,且互不阻塞。因此,理论上对同一域名的并发请求数限制不再是问题。
不放CDN时的情况
当你不使用 CDN,所有静态资源(JS, CSS, 图片, 字体等)都从你的源站域名(例如 www.example.com)加载时:
HTTP/1.1 环境下问题严重
- 假设你的页面有
20个静态资源(10个JS/CSS,10张图片)。 - 浏览器会同时发起最多
6个请求到www.example.com。 - 剩下的
14个请求必须排队等待。这会显著增加后续资源的等待时间,从而拖慢整个页面的加载速度,尤其是在高延迟的网络环境下。
视觉表现就是: 图片和内容会分批、一块一块地显示出来,而不是流畅地加载。
- 假设你的页面有
HTTP/2 环境下问题缓解,但仍有缺陷
- 虽然
HTTP/2的多路复用解决了队头阻塞和并发数限制,但所有请求仍然共享同一个TCP连接,并且全部指向你的源站服务器。 - 服务器压力集中:所有并发请求的压力都集中在源站服务器上。
- 失去地理优势:对于离你服务器很远的用户,所有资源的网络延迟都很高,
HTTP/2也无法解决物理距离带来的延迟问题。
- 虽然
CDN是如何解决这个问题的?
使用CDN后,你实际上是在创建多个域名或子域名来分发资源,这被称为域名分片。
场景:你的主站在
www.example.com,但你把静态资源放在了CDN上,地址可能是static.cdn-example.com或img.cdn-example.com。在HTTP/1.1下的工作原理:
- 浏览器对
www.example.com(主站HTML)有6个并发限制。 - 同时,浏览器对
static.cdn-example.com(JS/CSS)也有另外的6个并发限制。 - 同时,浏览器对
img.cdn-example.com(图片)还有另外的6个并发限制。 - 这样,总的并发请求能力就从6个提升到了
6 + 6 + 6 = 18个,资源可以并行下载,大大减少了排队时间。
- 浏览器对
在HTTP/2下的优势:
- 即使
HTTP/2本身不严格需要域名分片,但使用CDN带来的地理就近访问优势是巨大的。用户从CDN节点加载资源,延迟极低,这使得每个请求的完成速度都更快。 - 同时,将静态资源流量从源站分离,避免了源站服务器的连接数过载。
- 即使
对比总结表
| 场景 | HTTP/1.1 下的影响 | HTTP/2 下的影响 |
|---|---|---|
| 不放CDN (所有资源同域名) | 严重影响 并发请求被限制在~ 6个,资源加载严重排队,页面加载慢。 | 影响较小但存在 解决了并发限制,但所有请求延迟高,且源站压力大。 |
| 放CDN (资源在不同 CDN域名) | 显著改善 通过域名分片,有效突破并发限制,资源并行加载。 | 显著改善 利用 CDN的低延迟节点,每个请求速度更快,源站无压力。 |
结论与最佳实践
- 不放CDN,在HTTP/1.1环境下肯定会遇到严重的并发请求限制问题,这是前端性能的一个主要瓶颈。
- 即使全站升级到
HTTP/2,不使用CDN也意味着你放弃了降低网络延迟和分散服务器压力这两个最重要的性能优化手段。 - 最佳实践是结合使用:
- 使用CDN来托管静态资源,获得低延迟和高并发的好处。
- 在构建时,可以为不同类型的资源(如图片、
JS/CSS)分配不同的CDN子域名,以在必要时兼容HTTP/1.1。 - 确保你的服务器和
CDN都支持并启用了HTTP/2,以获得最佳性能。
全放CDN上又有什么问题?(不放CDN的“反向”问题)
把所有静态资源,不加区分地都放到CDN上,也会引入新的问题:
架构复杂性和运维成本
- 缓存策略复杂:你需要为不同类型的资源(如长期不变的
JS/CSS、频繁更新的商品图、敏感文件)配置不同的缓存过期时间、缓存键规则。 - 刷新失效挑战:当资源更新时,你需要手动或通过
API去刷新CDN缓存,以确保用户能获取到最新版本。如果刷新不及时或配置错误,会导致用户看到旧版本,即“缓存污染”问题。
- 缓存策略复杂:你需要为不同类型的资源(如长期不变的
安全性风险
- 攻击面扩大:
CDN的URL是公开的,如果你的资源链接被爬取或泄露,CDN会成为DDoS攻击的入口,虽然它能缓解一部分,但攻击流量依然会产生费用。 - 敏感信息泄露:如果将本应私有的、带权限验证的静态资源(如用户上传的合同、隐私图片)错误地放到了公共
CDN上,可能会导致严重的数据泄露。这类资源绝对不能放公共CDN。
- 攻击面扩大:
成本问题
- CDN流量费用:虽然单价低,但总量巨大时,
CDN费用也会非常可观。 - 回源流量费用:当
CDN节点缓存未命中时,需要回源站拉取资源,这部分流量(回源流量)也是需要付费的。如果资源更新过于频繁或缓存配置不当,会导致大量回源,成本不降反升。
- CDN流量费用:虽然单价低,但总量巨大时,
潜在的稳定性问题
- CDN服务商故障:虽然概率极低,但一旦你的
CDN服务商出现全局性故障,你的网站静态资源将全部无法加载。你需要有降级方案(例如,在检测到CDN失败时,动态地将资源URL切换回源站)。
- CDN服务商故障:虽然概率极低,但一旦你的
最佳实践与总结
| 资源类型 | 推荐做法 | 原因 |
|---|---|---|
| 公共前端资源 ( JS, CSS, 字体, 公共图片) | 强烈推荐放CDN | 用户受益最大,缓存效果好,能显著提升网站性能。 |
| 用户生成内容 ( UGC, 如用户头像、商品图) | 推荐放CDN | 量大,访问分散,非常适合CDN分发。但需注意图片处理(缩略图等)。 |
| 大文件 ( App安装包、视频) | 必须放CDN | 节省源站带宽,加速下载,是CDN的核心应用场景。 |
| 私密/敏感文件 (付费内容、用户合同) | 不放公共CDN,或使用 带鉴权的私有CDN | 安全第一。公共CDN无法保证隐私,必须通过源站或专门的私有资源服务进行权限校验。 |
| 频繁实时更新的小文件 (如 JSON配置, 实时行情图) | 谨慎使用CDN,或设置很短缓存 | 缓存反而会导致数据延迟。如果对实时性要求极高,可能直接回源更快。 |
| 开发/测试环境资源 | 通常不放CDN | 避免不必要的成本,且需要频繁更新,缓存会带来调试困扰。 |
结论
采用“动静分离”的架构是基本原则。 将公共的、不变的、量大的静态资源交给CDN,以提升性能和降低成本;而将动态的、私密的、实时性要求极高的资源保留在源站或通过专门的、安全的服务来处理。
不要走极端,“全放”和“全不放”都是不合理的。正确的做法是根据业务场景和资源特性,制定精细化的CDN使用策略。
SPA 如何解决 SEO 问题?
前端 SPA(单页应用)项目在 SEO 方面确实存在天然的劣势,但通过一系列现代技术和方法,可以完全解决或大幅改善这个问题。
以下从 问题的根源、解决方案 和 实践建议 三个方面来详细阐述。
一、 为什么 SPA 对 SEO 不友好?
- 内容异步加载:
SPA的核心是JavaScript。浏览器初始加载的只是一个近乎空的HTML外壳(一个<div id="app"></div>)和一堆JS文件。页面的真实内容是通过JS执行后,再通过Ajax异步请求数据并渲染到页面上的。 - 搜索引擎爬虫的抓取机制:
- 传统爬虫(如早期的 Googlebot):它们像是一个没有
JavaScript引擎的浏览器,只能看到初始的 HTML 外壳,无法获取异步加载的内容。因此,它们认为你的页面是“空”的。 - 现代爬虫(如当前的 Googlebot):虽然
Google宣称其爬虫已经能够执行JavaScript并等待页面渲染,但这个过程依然存在挑战:- 渲染延迟:爬虫资源有限,它不会像真实用户那样等待很长时间。如果你的应用过大、
JS文件加载过慢,爬虫可能等不及内容渲染完成就离开了。 - 复杂性:复杂的
JS应用(如状态管理、路由懒加载)可能会让爬虫解析起来更加困难,导致索引不准确。 - 其他搜索引擎:并非所有搜索引擎(如
Bing、百度、俄语的Yandex)都像Google一样能很好地处理JS。你的网站在这些搜索引擎中可能依然表现不佳。
- 渲染延迟:爬虫资源有限,它不会像真实用户那样等待很长时间。如果你的应用过大、
- 传统爬虫(如早期的 Googlebot):它们像是一个没有
二、 主流的解决方案
针对以上问题,业界主要有三种主流解决方案,下图清晰地展示了它们的工作原理与适用场景:

下面我们来详细看看每种方案:
方案一:服务器端渲染(SSR - Server-Side Rendering)
SSR 是指在服务器上就运行你的 JavaScript 应用,生成完整的 HTML 内容,然后直接发送给客户端(浏览器或爬虫)。
- 工作原理:当用户或爬虫请求一个页面时,服务器会立即执行
React/Vue代码,生成最终的HTML,而不是一个空壳。这个完整的HTML会直接被返回。之后,客户端会接管这些静态内容,并使其“活化”,恢复所有的交互功能(这个过程叫Hydration)。 - 优点:
- 极佳的 SEO:爬虫第一时间就能获取到完整内容,无需等待
JS执行。 - 更快的首屏加载速度:用户能更快地看到页面内容,尤其是对于慢网络或低性能设备。
- 极佳的 SEO:爬虫第一时间就能获取到完整内容,无需等待
- 缺点:
- 服务器成本高:每次请求都需要服务器运行
JS,消耗CPU资源。 - 开发复杂度:需要维护一个
Node.js服务器环境,并处理服务端和客户端的差异。
- 服务器成本高:每次请求都需要服务器运行
- 技术栈:
- Next.js(React):目前最流行、生态最完善的
SSR框架。 - Nuxt.js(Vue):
Vue生态中最主流的SSR框架。 - Angular Universal(Angular):
Angular的SSR解决方案。
- Next.js(React):目前最流行、生态最完善的
方案二:静态站点生成(SSG - Static Site Generation)
SSG 是 SSR 的一种更极致的形态。它不是在用户请求时生成 HTML,而是在项目构建时就提前生成所有页面的静态 HTML 文件。
- 工作原理:在
npm run build阶段,框架会运行你的应用,将所有路由对应的页面预先渲染成一个个独立的.html文件。部署时,只需要将这些静态文件放到任何Web服务器(如Nginx、CDN)上即可。 - 优点:
- 性能极致:由于是纯静态文件,访问速度极快,服务器压力极小。
- 安全性高:没有动态服务器逻辑,攻击面更小。
- SEO 效果好:和
SSR一样,爬虫直接获取完整HTML。
- 缺点:
- 只适用于内容变化不频繁的页面:如果页面数据需要频繁更新(如用户 dashboard、社交动态),每次更新都需要重新构建和部署整个站点。
- 技术栈:
- Next.js、Gatsby(React):同时支持
SSG和SSR。 - Nuxt.js、VitePress(Vue):同样支持
SSG。 - Hexo、Jekyll(传统静态站点生成器)。
- Next.js、Gatsby(React):同时支持
方案三:动态渲染(Dynamic Rendering)
这是一种“取巧”的方案,不直接修改你的 SPA 应用,而是通过一个中间层来识别请求来源。
- 工作原理:
- 当请求到达服务器时,先判断
User-Agent(用户代理)是普通浏览器还是搜索引擎爬虫。 - 如果是爬虫,则用一个无头浏览器(如
Puppeteer,Playwright)来访问你的SPA页面,执行JS并渲染出完整HTML,然后将这个HTML返回给爬虫。 - 如果是普通用户,则正常返回
SPA的空壳和JS文件。
- 当请求到达服务器时,先判断
- 优点:
- 对现有 SPA 项目改造成本最低,无需重写代码为
SSR/SSG。
- 对现有 SPA 项目改造成本最低,无需重写代码为
- 缺点:
- 可能被视为 Cloaking(伪装):如果提供给用户和爬虫的内容不一致,可能会被搜索引擎惩罚。但
Google官方表示,这种为了兼容性的动态渲染是允许的。 - 增加了架构复杂性:需要维护一个渲染服务。
- 可能被视为 Cloaking(伪装):如果提供给用户和爬虫的内容不一致,可能会被搜索引擎惩罚。但
- 技术方案:
- 可以使用 Rendertron 或 Puppeteer 自建服务。
- 一些第三方服务也提供此功能。
三、 SEO 最佳实践(无论采用何种方案)
即使你选择了 SSR/SSG,以下细节也同样重要:
合理的 Meta 标签管理:
- 每个页面都应有独一无二的
<title>、<meta name="description">和<meta name="keywords">(后者重要性已降低)。 - 使用 React Helmet(
React)或 Vue Meta(Vue)等库来在组件内轻松管理这些标签。
- 每个页面都应有独一无二的
语义化的 HTML 结构:
- 正确使用
<h1>~<h6>、<section>、<article>、<nav>等标签,帮助爬虫理解页面结构。
- 正确使用
优化性能和加载速度:
- 压缩图片、代码分割、懒加载。一个加载缓慢的页面,即使用
SSR,也会影响搜索排名。
- 压缩图片、代码分割、懒加载。一个加载缓慢的页面,即使用
使用规范的 URL 结构:
- 确保每个页面都有唯一且清晰的
URL。在SPA中,要使用History模式(而不是 Hash 模式#)的路由。
- 确保每个页面都有唯一且清晰的
构建并提交 Sitemap:
- 生成
sitemap.xml文件,列出网站所有重要的URL,并提交到Google Search Console等平台。
- 生成
设置合理的 Robots.txt:
- 指导爬虫哪些页面可以抓取,哪些不可以。
使用结构化数据:
- 通过 JSON-LD 等方式在页面中添加结构化数据,帮助搜索引擎更好地理解页面内容,从而获得更丰富的搜索结果展示(如星级评分、面包屑导航等)。
总结与选择建议
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| SSR | 首屏快,SEO 极佳,内容实时 | 服务器成本高,开发复杂 | 高动态内容、强 SEO 需求的 Web 应用(如社交、电商) |
| SSG | 性能极致,安全,SEO 极佳 | 内容更新需重新构建 | 内容相对固定的网站(如官网、博客、文档) |
| 动态渲染 | 对现有 SPA 改造快 | 有风险,架构复杂 | 旧 SPA 项目快速提升 SEO,且无法立即重构 |
| 纯 SPA | 开发体验好,交互流畅 | SEO 差,首屏慢 | 无需考虑 SEO 的后台管理系统、登录后的应用界面 |
给你的建议:
- 如果是新项目:优先考虑 Next.js(React) 或 Nuxt.js(Vue)。它们默认支持
SSG/SSR,开箱即用,能让你在享受SPA开发体验的同时,无缝获得优秀的SEO能力。 - 如果是现有 SPA 项目:评估重构成本。如果
SEO至关重要,建议逐步迁移到Next.js/Nuxt.js。如果只是需要快速提升,可以尝试 动态渲染 作为临时方案。 - 如果完全不需要 SEO:比如公司内部后台系统,那么纯
SPA是完全没问题的。
SPA & SSR & SSG
1. SPA(单页应用 Single-Page Application)
概念
SPA 是在客户端动态渲染的应用程序,初始加载后,所有的页面切换都在浏览器中完成,不需要重新加载整个页面。
特点
- 客户端渲染:在浏览器中通过
JavaScript动态生成内容 - 前后端分离:前端负责
UI渲染,后端提供API接口 - 交互体验好:页面切换流畅,无刷新体验
- 首次加载慢:需要下载所有必要的
JavaScript文件
代码示例
Vue3 SPA 示例
<template>
<div>
<h1>Vue SPA 应用</h1>
<nav>
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
</nav>
<router-view />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
const data = ref(null)
const route = useRoute()
onMounted(async () => {
// 客户端获取数据
try {
const response = await fetch('/api/data')
data.value = await response.json()
} catch (error) {
console.error('获取数据失败:', error)
}
})
</script>2. SSR(服务端渲染 Server-Side Rendering)
概念
SSR 是在服务器端生成完整的 HTML 页面,然后发送到客户端显示。
特点
- 服务端渲染:
HTML在服务器端生成 - 首屏加载快:用户直接看到完整内容
- SEO 友好:搜索引擎可以直接抓取内容
- 服务器压力大:每次请求都需要服务器渲染
代码示例
Nuxt.js SSR 示例
<!-- pages/blog/[id].vue -->
<template>
<div>
<h1>{{ post.title }}</h1>
<div>{{ post.content }}</div>
<p>发布于: {{ post.date }}</p>
</div>
</template>
<script setup>
// 服务端数据获取
const { data: post } = await useAsyncData('post', () =>
$fetch(`/api/posts/${route.params.id}`)
)
// 或者使用 useFetch 简写
const { data: post } = await useFetch(`/api/posts/${route.params.id}`)
// SEO 配置
useSeoMeta({
title: post.value?.title,
description: post.value?.description
})
</script>服务端渲染流程
// 1. 接收请求
app.get('*', async (req, res) => {
// 2. 数据获取(不涉及 DOM)
const data = await fetchData(req.url)
// 3. 创建 Redux store(纯 JavaScript)
const store = createStore(reducer, data)
// 4. 虚拟 DOM 渲染(无真实 DOM)
const appVirtualTree = (
<Provider store={store}>
<App />
</Provider>
)
// 5. 虚拟 DOM 转 HTML 字符串
const htmlContent = ReactDOMServer.renderToString(appVirtualTree)
// 6. 组装完整 HTML
const fullHTML = `
<!DOCTYPE html>
<html>
<body>
<div id="root">${htmlContent}</div>
<script>
// 将数据传递给客户端
window.__PRELOADED_STATE__ = ${JSON.stringify(store.getState())}
</script>
</body>
</html>
`
res.send(fullHTML)
})为什么需要客户端注水?
// 服务端渲染结果:
// <button>点击我</button> ← 静态 HTML,没有事件
// 客户端注水后:
// <button onclick="handleClick">点击我</button> ← 添加了交互能力SSR 渲染的本质
- ✅ 不是操作真实
DOM- 服务端没有DOMAPI - ✅ 是虚拟
DOM到字符串的序列化 - 纯字符串操作 - ✅ 只执行渲染相关的生命周期 - 不执行
DOM依赖的方法 - ✅ 结果是静态
HTML字符串 - 没有事件绑定等交互功能
总结
SSR 在服务端能够渲染 HTML 的秘诀就是:
- 虚拟
DOM技术 - 用JavaScript对象描述UI结构 - 字符串序列化 - 将虚拟
DOM转换为HTML字符串 - 最小环境模拟 - 只模拟渲染必需的环境,不实现完整
DOM - 纯计算过程 - 整个过程都是
JavaScript计算,不依赖浏览器API
3. SSG(静态站点生成 Static Site Generation)
概念
SSG 在构建时预渲染页面,生成静态 HTML 文件,直接部署到 CDN。
特点
- 构建时渲染:在构建阶段生成静态页面
- 访问速度极快:直接服务静态文件
- SEO 友好:内容完全预渲染
- 内容更新需要重新构建:不适合频繁更新的内容
代码示例
Nuxt.js SSG 示例
<!-- pages/blog/index.vue -->
<template>
<div>
<h1>博客文章</h1>
<div v-for="post in posts" :key="post.id" class="post">
<h2>
<NuxtLink :to="`/blog/${post.slug}`">{{ post.title }}</NuxtLink>
</h2>
<p>{{ post.excerpt }}</p>
</div>
</div>
</template>
<script setup>
// 构建时获取数据
const { data: posts } = await useAsyncData('posts', () =>
queryContent('blog')
.sort({ date: -1 })
.find()
)
// 配置静态生成
definePageMeta({
// 可以添加页面元信息
})
</script>三者的详细对比
| 特性 | SPA | SSR | SSG |
|---|---|---|---|
| 渲染位置 | 客户端 | 服务端 | 构建时 |
| 渲染方式 | 在浏览器中动态渲染 | 在服务器端渲染成HTML | 在构建时预渲染成HTML |
| 首屏加载 | 较慢 | 快 | 极快 |
| SEO | 差 | 好 | 好 |
| 服务器压力 | 小 | 大 | 无 |
| 内容更新 | 实时 | 实时 | 需重新构建 |
| 开发复杂度 | 低 | 中 | 低 |
| 适用场景 | 后台系统、交互应用 | 电商、新闻、社交 | 博客、文档、官网 |
选择建议
使用 SPA 当:
- 需要丰富的交互体验
- 对
SEO要求不高 - 主要是用户后台管理系统
- 团队熟悉前端框架
使用 SSR 当:
- 需要良好的
SEO - 内容频繁更新
- 需要首屏快速加载
- 用户访问分布广泛
使用 SSG 当:
- 内容相对固定
- 需要极快的访问速度
- 对
SEO要求高 - 希望降低服务器成本
现代框架的混合使用
现代框架如 Next.js、Nuxt.js 支持混合模式:
// Next.js 混合示例
export default function HybridPage({ data }) {
return <div>{data}</div>
}
// 部分页面使用 SSG
export async function getStaticProps() { /* ... */ }
// 部分页面使用 SSR
export async function getServerSideProps() { /* ... */ }
// 部分页面使用客户端渲染
// 不导出任何 getStaticProps 或 getServerSideProps这样可以根据不同页面的需求选择最合适的渲染策略,在性能、SEO 和用户体验之间取得最佳平衡。