Web Performance

为什么你的网站应该发送更少请求,而不是只缩小文件

小文件并不是免费的。现代 HTTP 降低了请求开销,但没有让它变得无关紧要。

The Wux Webtools Team The Wux Webtools Team 9 分钟阅读 人工智能辅助,人工审核
A stylized browser waterfall showing many small requests simplified into fewer intentional requests.
目录
  1. “把每个文件都变小就行”的舒适迷思
  2. 请求不只是字节
  3. “但 HTTP/2 已经解决了这个问题”,大多并没有
  4. waterfall 才是真相所在
  5. 更小的文件仍然重要——只是重要性并不相同
  6. 许多小文件的隐藏成本
  7. 1. 发现太晚
  8. 2. Header 开销
  9. 3. 主线程打断
  10. 4. 缓存复杂性
  11. Bundling 回来了,但需要判断力
  12. 第三方请求值得额外怀疑
  13. 一个实用的请求减少检查清单
  14. 移除
  15. 有判断地合并
  16. 延后
  17. 正确缓存
  18. 重新测量
  19. 好的状态是什么样

“把每个文件都变小就行”的舒适迷思

多年来,Web 性能建议听起来很简单:压缩一切,minify 一切,让每个资源都更小。

这个建议在大方向上仍然正确。40 KB 的脚本通常比 400 KB 的脚本更好。优化过的图片比原始导出更好。Brotli、AVIF、CSS minification、tree shaking 和字体子集化都很重要。

但在许多生产网站上,更大的问题不再是某个文件过大,而是在页面感觉可用之前,浏览器必须请求的东西太多。

一个页面的文件大小看起来可能很克制,却仍然很慢,因为它发送了 120 个请求:CSS 片段、JavaScript 分块、第三方标签、字体文件、tracking pixels、图标 sprites、JSON endpoints、preloads、analytics beacons 和缓存重新验证。每一个都可能“很小”。合在一起,它们会形成一条漫长而脆弱的 waterfall。

实用规则是:当单个资源已经得到合理压缩后,减少请求数量通常比从每个文件里再削掉几个 KB 更能改善用户体验。

请求不只是字节

一次网络请求不只是数据传输。它是一连串工作。

浏览器必须发现资源,决定何时获取它,将它与其他资源一起调度,发送 headers,等待服务器,接收 headers,解析响应,通常还要解压,然后用它做一些有用的事。

那个“有用的事”可能很昂贵。JavaScript 文件必须被解析、编译和执行。CSS 文件可能阻塞渲染。字体文件可能延迟文本可读时间,或导致布局偏移。图片可能影响 Largest Contentful Paint 元素。第三方脚本可能带来自己的依赖链。

这就是为什么即使文件很小,请求数量仍然重要。一个 3 KB 的脚本,如果它阻塞渲染、到达很晚,并且在错误的时刻于主线程执行,可能比一张 30 KB 的图片更糟。

如果你正在阅读 Lighthouse 报告,并觉得自己被几十条分散的警告惩罚了,先看请求 waterfall,而不是单个分数。我们在如何不慌张地阅读 Lighthouse 报告中有一个实用演练,但简短版本是:找出什么阻塞了首次渲染,什么延迟了主要内容。

“但 HTTP/2 已经解决了这个问题”,大多并没有

HTTP/2 和 HTTP/3 改变了请求的成本结构。它们引入了 multiplexing、header compression 和更好的连接行为。简单来说,浏览器变得更擅长通过更少的连接发送多个请求。

这是一次真正的改进。它也终结了一些旧习惯,比如极端的 CSS sprites,以及仅仅为了规避连接数限制而构建的巨大 concatenated bundles。

但 HTTP/2 并没有让请求变得免费。

当许多资源共享一个连接时,multiplexing 会有帮助,但浏览器仍然必须为它们确定优先级。服务器仍然必须响应。客户端仍然必须处理每个响应。拥塞、丢包、TLS negotiation、DNS lookup、缓存未命中和主线程压力仍然存在。

HTTP/3 改进了一些传输行为,尤其是连接迁移和传输层 head-of-line blocking 方面。它并没有移除发现、调度、下载、解析和执行资源的成本。

因此,现代目标不是“把所有东西打进一个巨大的文件”。而是“发送更少的关键请求,并让剩下的请求都有明确目的”。

waterfall 才是真相所在

性能问题很少以单一指标的形式宣布自己。它们会以形状呈现出来。

打开浏览器的 network panel,观察最初几秒。问问自己:

  • 主内容出现之前,有多少请求已经开始?
  • 哪些请求阻塞了渲染?
  • 重要资源是否被发现得太晚?
  • 第三方脚本是否在与第一方 CSS、字体或图片竞争?
  • 是否有许多文件返回 304 响应,而不是直接从缓存提供?
  • 图标、字体或 UI 片段是否被拆成了比页面实际需要更多的文件?

一个快速页面通常有一条乏味的早期 waterfall。少量关键资源很早到达。非关键资源等待。第三方脚本被延迟、限制或移除。浏览器不必在能够绘制页面之前被迫同时处理二十个优先级。

一个慢页面往往有一条紧张的 waterfall:许多小文件、许多来源,以及许多迟来的发现。

更小的文件仍然重要——只是重要性并不相同

这不是反对压缩或优化。这是在反对只优化字节,却忽视协同。

当资源很大、阻塞渲染,或位于主内容路径上时,更小的文件最重要。例如:

  • hero image 应该使用合适的尺寸和编码。
  • 阻塞渲染的 CSS 应该保持精简。
  • 首次交互所需的 JavaScript 应该最小化。
  • 字体应该进行子集化、压缩,并限制为实际使用的字重。

字体是一个常见例子。团队常常纠结一个字体文件是 24 KB 还是 31 KB,却同时发布六种字重、两种样式和多个字体族。更好的修复方式不是从一个文件里削掉 7 KB,而是发送更少的字体文件。如果排版是你性能工作的一部分,web fonts 仍然是大多数网站上最容易获得的性能收益之一

图片也遵循同样的模式。AVIF 或 WebP 可以节省有意义的字节,但在首屏发送十张装饰性图片仍然不是好计划。选择更好的格式当然重要,但也要质疑每张图片是否真的需要被请求。关于格式选择,我们的指南 AVIF 何时胜过 WebP,以及何时不会 可以作为这项请求数量工作的有用补充。

许多小文件的隐藏成本

许多小请求往往会制造一些问题;如果你只看总传输字节数,这些问题不会显现出来。

1. 发现太晚

浏览器无法请求它尚未发现的东西。CSS 文件可以引用字体。脚本可以 import 另一个脚本。组件可以在 hydration 之后请求 JSON。每个依赖都会在链条中增加一步。

链条越深,重要工作开始得越晚。

2. Header 开销

每个请求和响应都包含 headers。Header compression 会有帮助,尤其是在 HTTP/2 和 HTTP/3 上,但它不会消除开销。Cookies 会让情况更糟。如果你的网站在每个请求中都发送大型 cookies,小资源在实践中就不再那么小。

这也是静态资源通常应该放在无 cookie 的路径或域名上的原因之一,也是 cache headers 值得关注的原因。如果 headers 在生产环境中的行为很奇怪,调试 redirects 和 HTTP headers 通常比猜测更快。

3. 主线程打断

许多 JavaScript chunks 可能造成反复的解析和执行工作。即使每个 chunk 都很小,浏览器也可能不断停下来评估代码。这会伤害 Interaction to Next Paint,并让页面感觉抖动。

用户并不在乎每个文件都很小。他们在乎的是点击菜单花了 600 毫秒。

4. 缓存复杂性

在谨慎处理时,拆分资源可以改善缓存。一个稳定的 vendor bundle 加上一个变化的 app bundle,可能是不错的拆分。

但过度 chunking 可能适得其反。更多文件意味着更多缓存查找、更多重新验证机会、更多版本协调,以及更多意外使本不需要变更的资源失效的方式。

Bundling 回来了,但需要判断力

Web 性能的第一个时代喜欢 bundling,因为浏览器有严格的连接限制。随后 HTTP/2 到来,许多团队又猛烈转向激进的 code splitting。其中一些是有用的,另一些则变成了迷信。

理性的中间路径是 route-aware bundling。

对于典型的营销网站或内容网站:

  • Inline 或仅加载初始渲染所需的 CSS。
  • 保持全局 JavaScript 很小。
  • 避免把微小模块拆成单独的网络请求。
  • 延迟不需要立即使用的交互功能。
  • 移除无法证明其成本合理的第三方脚本。

对于应用:

  • 按 route 或主要功能拆分,而不是按每个组件拆分。
  • 让共享依赖保持稳定且可缓存。
  • 只 preload 很快就一定会需要的资源。
  • 避免在公开页面加载 admin、dashboard、editor 或 experiment 代码。
  • 衡量交互成本,而不只是 bundle size。

Bundling 并不自动就是好的。Code splitting 也不自动就是好的。真正有用的问题是:这个拆分是否帮助浏览器更早交付下一个有意义的用户体验?

第三方请求值得额外怀疑

第一方请求至少在你的控制之下。第三方请求往往比看起来更慢、更不可预测,也更昂贵。

一个 tag manager 就可能触发 analytics、ads、heatmaps、chat widgets、A/B testing、consent tools 和 personalization scripts。每个 vendor 都可能带来更多请求。有些会很早运行。有些会阻塞主线程。有些会在不经过你发布流程的情况下改变。

最好的第三方优化是删除。第二好的方式是延迟。

在添加第三方脚本之前,问问:

  • 它是否需要在用户看到页面之前加载?
  • 它是否需要在每个页面加载?
  • 它能否在 consent、interaction 或 idle time 之后加载?
  • 内部由谁负责它?
  • 哪个指标能证明它值得付出性能成本?

这就是性能成为治理问题的地方。必须有人被允许说不。

一个实用的请求减少检查清单

从最重要的页面开始:首页、定价页、产品页、结账页、注册页或主要落地页。然后逐项检查 waterfall。

移除

  • 删除未使用的 JavaScript 和 CSS。
  • 移除旧实验、废弃的 pixels 和重复的 analytics。
  • 去掉未使用的字体字重和图标库。
  • 在合适的情况下,用 CSS 替代装饰性图片。

有判断地合并

  • 将总是一起加载的微小 JavaScript 模块打包。
  • 合并阻塞同一渲染路径的小 CSS 文件。
  • 当 SVG sprites 或 inline SVG 能减少请求且不损害可维护性时,用于重复图标。

延后

  • Lazy-load 首屏以下的图片。
  • 将非关键脚本延迟到 first paint 或用户交互之后。
  • 仅在需要时加载评论、embeds、地图、聊天和视频播放器。

正确缓存

  • 对带版本的静态资源使用长期缓存。
  • 避免对很少变化的文件进行不必要的重新验证。
  • 保持 HTML 新鲜,但让 hashed assets 保持缓存。

重新测量

每次变更后,再次检查 waterfall。目标不是完美分数。目标是更少的关键请求、更早的有用渲染,以及更少的主线程干扰。

好的状态是什么样

一个健康的页面不一定拥有尽可能少的请求。它拥有一条小而明确的关键路径。

浏览器拿到 HTML、必要的 CSS、如果有的话还包括主内容图片,也许还有一个导航或首屏交互所需的小脚本,以及让文本可读所需的最小字体集合。其他一切都等待自己的时机。

这就是一个只是被优化过的页面,和一个感觉很快的页面之间的区别。

缩小文件仍然值得做。但如果网站已经得到了合理压缩,下一个性能收益通常不是从某个 bundle 中再省下 2 KB,而是少一个阻塞请求、少一个字体文件、少一个第三方脚本、少一条依赖链。

更少的请求会让浏览器的工作更简单。简单往往意味着更快,只是我们并不总愿意承认这一点。

常见问题

一个大 bundle 比许多小文件更好吗?
不一定。一个巨大的 bundle 可能延迟所有内容,尤其是在首次加载时。许多微小文件则可能产生调度和执行开销。更好的模式是把总是一起需要的资源打包,并按 route 或主要功能拆分。
HTTP/2 是否意味着请求数量不再重要?
不是。HTTP/2 通过 multiplexing 和 header compression 降低了一些连接开销,但每个请求仍然有发现、优先级、服务器、缓存、解析和执行成本。
我应该 inline 所有 critical CSS 吗?
Inline 少量真正关键的 CSS 可以帮助首次渲染,但 inline 太多会让 HTML 更重,也更难缓存。保持最小化,并衡量效果。
最容易减少请求的地方在哪里?
字体和第三方脚本通常是最快的收益来源。许多网站发布了未使用的字体字重、重复的 analytics、旧 pixels、chat widgets 或不需要立即加载的 embeds。
一个页面应该有多少请求?
没有通用目标。小型内容页面应该只有很少的关键请求。复杂应用可能需要更多。重点是减少首次渲染之前以及主要交互路径之前的请求。

来源与进一步阅读

  1. MDN Web Docs: HTTP caching
  2. web.dev: Optimize Largest Contentful Paint
  3. RFC 9113: HTTP/2
  4. HTTP Archive Web Almanac: Page Weight
关于作者
The Wux Webtools Team

最后更新:

继续阅读