ch13-Optimizing Application Delivery

优化应用交付

HTTP,第 13 章

高性能浏览器网络依赖于一系列网络技术的协同工作(图 13-1),而我们应用的整体性能是这些技术各自表现的总和。

我们无法控制客户端与服务器之间的网络环境,也无法控制客户端硬件或其设备配置,但其余部分尽在掌握:服务器端的 TCP、TLS 和 QUIC 优化,以及数十种应用层优化——这些优化需要针对不同物理层的特性、正在使用的 HTTP 协议版本,以及通用应用最佳实践进行针对性调整。诚然,将所有环节都调优到位并非易事,但回报丰厚!让我们将这些知识融会贯通。

Figure 13-1. Optimization layers for web application delivery

图 13-1. Web 应用交付的优化层次


优化物理层与传输层

通信信道的物理属性为每个应用设定了硬性的性能上限:光速与客户端和服务器之间的距离决定了传播延迟,介质的选择(有线 vs 无线)则决定了每个数据包所承受的处理、传输、排队及其他延迟。事实上,大多数 Web 应用的性能受限于延迟而非带宽,而且虽然带宽速度持续提升,遗憾的是延迟却未必同步改善:

  • 延迟的诸多组成部分
  • 交付更高带宽与更低延迟
  • 延迟作为性能瓶颈

因此,虽然我们无法让比特传播得更快,但在传输层和应用层应用所有可能的优化以消除不必要的往返、请求,并最小化每个数据包传输的距离——即将服务器部署得更靠近客户端——至关重要。

每个应用都可以从针对无线网络物理层独特特性的优化中受益,在这些网络中延迟高而带宽始终珍贵。在 API 层,有线与无线网络之间的差异完全透明,但忽视它们必然导致性能不佳。在如何以及何时调度资源下载、埋点上报等方面进行简单优化,就能显著影响应用的实际延迟、电池续航和整体用户体验:

  • 优化 WiFi 网络
  • 优化移动网络

从物理层向上移动,我们必须确保每台服务器都配置为使用最新的 TCP、TLS 和 QUIC 最佳实践。优化底层协议确保每个客户端在与服务器通信时都能获得最佳性能——高吞吐量和低延迟:

  • 优化 TCP
  • 优化 TLS
  • 优化 QUIC 与 HTTP/3

最后,我们抵达应用层。无论从哪个角度衡量,HTTP 都是一个极其成功的协议。毕竟,它是数十亿客户端和服务器之间的通用语言,支撑着现代 Web。然而,它也是一个不完美的协议,这意味着我们在架构应用时必须格外小心:

  • 我们必须规避 HTTP/1.x 的局限性。
  • 我们必须利用 HTTP/2 的新性能能力。
  • 我们必须为 HTTP/3 的未来做好准备。
  • 我们必须警惕地应用常青性能最佳实践。

成功 Web 性能策略的秘诀很简单:投资于监控和测量工具以识别问题和回归(参见合成监控与真实用户性能测量),将业务目标与性能指标关联,并据此优化——即将性能视为一项功能。


常青性能最佳实践

无论网络类型或网络协议的类型和版本如何,所有应用都应始终致力于消除或减少不必要的网络延迟,并最小化传输的字节数。这两条简单规则是所有常青性能最佳实践的基础:

减少 DNS 查询

每次主机名解析都需要一次网络往返,给请求带来延迟并在查询进行时阻塞请求。

重用 TCP 连接

尽可能利用连接 keepalive 以消除 TCP 握手和慢启动的延迟开销;参见慢启动。

最小化 HTTP 重定向次数

HTTP 重定向带来高延迟开销——例如,一次到不同源的重定向可能导致 DNS、TCP、TLS 和请求-响应往返,增加数百到数千毫秒的延迟。最优的重定向次数是零。

减少往返时间

将服务器部署得更靠近用户,通过减少往返时间(例如更快的 TCP 和 TLS 握手)来提升协议性能,并改善静态和动态内容的传输吞吐量;参见未缓存源站获取。

消除不必要的资源

没有比不发送请求更快的请求。警惕地审计并移除不必要的资源。

至此,这些建议应该无需过多解释:延迟是瓶颈,最快的发送字节是从未发送的字节。然而,HTTP 提供了额外的机制,如缓存和压缩,以及其版本特定的性能特性:

在客户端缓存资源

应用资源应被缓存,以避免每次需要资源时重新请求相同的字节。

传输时压缩资源

应用资源应以最少的字节数传输:始终为每个传输的资源应用最佳压缩方法。

消除不必要的请求字节

减少传输的 HTTP 头部数据(例如 HTTP Cookie)可以节省整个网络往返的延迟。

并行化请求和响应处理

客户端和服务端的请求和响应排队延迟常常被忽视,但却贡献了显著且不必要的延迟。

应用协议特定的优化

HTTP/1.x 提供有限的并行性,这要求我们必须打包资源、跨域分发交付等。相比之下,HTTP/2 和 HTTP/3 在使用单一连接时表现最佳,应移除 HTTP/1.x 特定的优化。

每一项都值得我们深入审视。让我们深入探讨。


在客户端缓存资源

最快的网络请求是未发出的请求。维护先前下载数据的缓存允许客户端使用资源的本地副本,从而消除请求。对于通过 HTTP 交付的资源,确保设置了适当的缓存头部:

  • Cache-Control 头部可以指定资源的缓存生命周期(max-age)。
  • Last-ModifiedETag 头部提供验证机制。

只要可能,你应为每个资源指定明确的缓存生命周期,这允许客户端使用本地副本,而不是一直重新请求相同的对象。同样,指定验证机制以允许客户端检查过期资源是否已更新:如果资源未改变,我们可以消除数据传输。

最后,注意你需要同时指定缓存生命周期和验证方法!一个常见错误是只提供其中之一,这会导致要么在资源未改变时进行冗余传输(即缺少验证),要么每次使用资源时都进行冗余验证检查(即缺少或不必要的短缓存生命周期)。

关于优化缓存策略的实践建议,参见 Google Web Fundamentals 的 “HTTP 缓存” 部分。


智能手机上的 Web 缓存:理想与现实

HTTP 资源的缓存自 HTTP 协议早期版本以来一直是顶级性能优化之一。然而,虽然似乎每个人都清楚其好处,但现实世界的研究持续发现它仍然是一个经常被忽视的优化!AT&T 实验室研究与密歇根大学的一项近期联合研究报告:

我们的发现表明,在两个数据集中,冗余传输分别占总 HTTP 流量的 18% 和 20%。在第二个数据集中,它们还占所有蜂窝数据流量的 17% 字节、7% 无线电能耗、6% 信令负载和 9% 无线电资源利用率。大多数此类冗余传输是由智能手机 Web 缓存实现未完全支持或严格遵守协议规范,或开发者未充分利用库提供的缓存支持造成的。

——《智能手机上的 Web 缓存》,MobiSys 2012

你的应用是否在不断获取不必要的资源?正如证据所示,这不是一个修辞性问题。仔细检查你的应用,更好的是,添加一些测试以在未来捕获任何回归。


压缩传输数据

利用本地缓存允许客户端避免在每次请求时获取重复内容。然而,如果必须获取资源——无论是因为它已过期、是新的,还是无法缓存——那么它应以最少的字节数传输。始终为每个资源应用最佳压缩方法。

文本类资源(如 HTML、CSS 和 JavaScript)使用 Gzip 或 Brotli 压缩时,平均可减少 60%–80% 的大小。另一方面,图像需要更细致的考虑:

  • 图像通常携带大量可剥离的元数据——例如 EXIF。
  • 图像应调整为其显示宽度以最小化传输字节。
  • 图像可以使用不同的有损和无损格式压缩。

图像占平均页面传输字节的一半以上,这使它们成为高价值的优化目标:选择最优图像格式这一简单决策可以产生显著改善的压缩比;有损压缩方法可以将传输大小减少数个数量级;将图像调整为其显示宽度将减少客户端的传输和内存占用(参见计算图像内存需求)。投资于工具和自动化以优化站点的图像交付。

关于减少文本、图像、Web 字体和其他资源的传输大小的实践建议,参见 Google Web Fundamentals 的 “优化内容效率” 部分。


消除不必要的请求字节

HTTP 是无状态协议,这意味着服务器不需要在不同请求之间保留关于客户端的任何信息。然而,许多应用需要状态来管理会话、个性化、分析等。为实现此功能,HTTP 状态管理机制(RFC 6265)扩展允许任何网站为其源关联和更新 “Cookie” 元数据:提供的数据由浏览器保存,然后自动附加到每个对源的请求的 Cookie 头部。

标准未指定 Cookie 大小的最大限制,但实际上大多数浏览器强制执行 4 KB 限制。然而,标准也允许每个源关联多个 Cookie。结果,可能为每个源关联数十到数百 KB 的任意元数据,分散在多个 Cookie 中!

不用说,这可能对你的应用产生显著的性能影响。关联的 Cookie 数据由浏览器在每个请求上自动发送,在最坏情况下,无论使用 HTTP/1.x 还是 HTTP/2,都可能通过超过初始 TCP 拥塞窗口而增加整个网络往返的延迟:

  • 在 HTTP/1.x 中,所有 HTTP 头部(包括 Cookie)在每个请求上以未压缩形式传输。
  • 在 HTTP/2 和 HTTP/3 中,头部使用 HPACK 或 QPACK 压缩,但至少 Cookie 值在第一个请求上传输,这将影响你的初始页面加载性能。

Cookie 大小应审慎监控:传输最小量的必要数据,如安全会话令牌,并在服务器上利用共享会话缓存查找其他元数据。更好的是,尽可能完全消除 Cookie——很可能在请求静态资源(如图像、脚本和样式表)时你不需要客户端特定的元数据。

在使用 HTTP/1.x 时,一个常见的最佳实践是指定一个专用的 “无 Cookie” 源,用于交付不需要客户端特定优化的响应。在 HTTP/2 和 HTTP/3 时代,这一实践的重要性有所降低,但仍值得考虑,特别是对于第三方静态资源。


并行化请求和响应处理

要在应用内实现最快的响应时间,所有资源请求都应尽快发出。然而,另一个重要考虑点是这些请求将如何在服务器上处理。毕竟,如果我们所有的请求然后被服务器串行排队,那么我们又招致了不必要的延迟。以下是获得最佳性能的方法:

  • 通过优化连接 keepalive 超时来重用 TCP 连接。
  • 在需要并行下载时使用多个 HTTP/1.1 连接。
  • 升级到 HTTP/2 或 HTTP/3 以启用多路复用和最佳性能。
  • 分配足够的服务器资源以并行处理请求。

没有连接 keepalive,每个 HTTP 请求都需要新的 TCP 连接,这由于 TCP 握手和慢启动而招致显著开销。确保识别并优化你的服务器和代理连接超时,以避免过早关闭连接。在此基础之上,为获得最佳性能,使用 HTTP/2 或 HTTP/3 以允许客户端和服务器为所有请求重用相同连接。如果 HTTP/2/3 不可行,使用多个 TCP 连接以通过 HTTP/1.x 实现请求并行性。

识别不必要的客户端和服务器延迟来源既是一门艺术也是一门科学:检查客户端资源瀑布(参见分析资源瀑布)以及你的服务器日志。常见陷阱通常包括:

  • 客户端上的阻塞资源强制延迟资源获取;参见 DOM、CSSOM 和 JavaScript。
  • 代理和负载均衡器容量配置不足,强制延迟将请求交付(排队延迟)给应用服务器。
  • 服务器配置不足,强制慢速执行和其他处理延迟。

浏览器中的资源加载优化

浏览器将自动确定文档中每个资源的最优加载顺序,我们既可以协助也可以阻碍浏览器完成这一过程:

  • 我们可以提供提示来协助浏览器;参见浏览器优化。
  • 我们可以通过向浏览器隐藏资源来造成阻碍。

现代浏览器被设计为尽可能高效地尽早扫描 HTML 和 CSS 文件的内容。然而,文档解析器也经常被阻塞,等待脚本或其他阻塞资源下载后才能继续。在此期间,浏览器使用 “预加载扫描器”,它在源代码中投机性地提前查找可以提前分派以减少整体延迟的资源下载。

注意,预加载扫描器的使用是一种投机性优化,仅在文档解析器被阻塞时使用。然而,在实践中,它产生显著收益:基于 Google Chrome 的实验数据,它提供约 20% 的页面加载时间和渲染速度提升!

遗憾的是,这些优化不适用于通过 JavaScript 调度的资源;预加载扫描器不能投机性地执行脚本。结果,将资源调度逻辑移入脚本可能为应用提供更细粒度的控制,但这样做会将资源从预加载扫描器中隐藏,这是一个值得仔细权衡的取舍。


优化 HTTP/1.x

我们优化 HTTP/1.x 部署的顺序很重要:配置服务器以交付最佳的 TCP 和 TLS 性能,然后仔细审查并应用移动和常青应用最佳实践:测量,迭代。

在常青优化到位且应用内拥有良好的性能检测机制后,评估应用是否可以从应用 HTTP/1.x 特定优化(读作:协议变通方案)中受益:

利用 HTTP 管道化

如果你的应用同时控制客户端和服务器,管道化可以帮助消除不必要的网络延迟;参见 HTTP 管道化。

应用域名分片

如果你的应用性能受限于默认的每源六个连接限制,考虑将资源分散到多个源;参见域名分片。

打包资源以减少 HTTP 请求

诸如合并(concatenation)和精灵图(spriting)等技术都可以帮助最小化协议开销并交付类似管道化的性能收益;参见合并与精灵图。

内联小资源

考虑将小资源直接嵌入父文档以最小化请求数量;参见资源内联。

管道化支持有限,其余每项优化都有其收益和权衡。事实上,常被忽视的是,这些技术在激进或错误应用时都可能损害性能;深入讨论参见 HTTP/1.X。

HTTP/2 和 HTTP/3 消除了对上述所有 HTTP/1.x 变通方案的需求,使我们的应用既更简单性能更高。也就是说,HTTP/1.x 的最佳优化是部署 HTTP/2 或 HTTP/3。


优化 HTTP/2

HTTP/2 通过启用请求和响应多路复用、头部压缩、优先级等,实现了网络资源更高效的使用和延迟降低——参见设计和技术目标。在单一连接每源模型的背景下,从 HTTP/2 获得最佳性能需要良好调优的服务器网络栈。深入讨论和优化清单参见优化 TCP 和优化 TLS。

接下来——惊喜——应用常青应用最佳实践:发送更少的字节,消除请求,并针对无线网络调整资源调度。减少传输的数据量和消除不必要的网络延迟是任何应用的最佳优化,无论应用和传输协议的版本或类型如何。

最后,撤销并摒弃域名分片、资源合并和图像精灵图的坏习惯。有了 HTTP/2,我们不再受限于有限的并行性:请求变得廉价,请求和响应都可以高效地多路复用。这些变通方案不再需要,省略它们可以提升性能。


消除域名分片

HTTP/2 通过在同一 TCP 连接上多路复用请求来实现最佳性能,这实现了高效的请求和响应优先级、流控和头部压缩。结果,最优的连接数恰好是一,域名分片成为反模式。

HTTP/2 还提供 TLS 连接合并机制,允许客户端在满足以下条件时将来自不同源的请求合并并通过同一连接分派:

  • 这些源被相同的 TLS 证书覆盖——例如通配符证书,或具有匹配 “使用者备用名称” 的证书。
  • 这些源解析到相同的服务器 IP 地址。

例如,如果 example.com 提供对其所有子域有效的通配符 TLS 证书(即 *.example.com),并引用解析到与 example.com 相同服务器 IP 地址的 static.example.com 上的资源,那么 HTTP/2 客户端被允许重用相同的 TCP 连接来获取来自 example.com 和 static.example.com 的资源。

HTTP/2 连接合并的一个有趣副作用是它启用了一种对 HTTP/1.x 友好的部署模型:一些资源可以从替代源提供,这为 HTTP/1 客户端启用了更高的并行性,如果这些相同的源满足上述标准,HTTP/2 客户端可以合并请求并重用相同连接。或者,应用可以更主动地检查协商的协议并为每个客户端交付替代资源:为 HTTP/1.x 客户端提供分片的资源引用,为 HTTP/2 客户端提供同源资源引用。

根据应用的架构,你可能可以依赖连接合并,你可能需要服务替代标记,或者你可能根据需要使用两种技术来为 HTTP/1.x 和 HTTP/2 提供最优体验。或者,你可能考虑只专注于优化 HTTP/2 和 HTTP/3 性能;客户端采用率正在快速增长,为两种协议优化的额外复杂性可能是不必要的。

由于第三方依赖,可能无法通过相同 TCP 连接获取所有资源——这没关系。无论协议如何,寻求最小化源的数量,并在使用 HTTP/2 或 HTTP/3 时消除分片以获得最佳性能。


最小化资源合并与图像精灵图

将多个资源打包到单一响应中是 HTTP/1.x 的关键优化,其中有限的并行性和高协议开销通常胜过所有其他考虑——参见合并与精灵图。然而,有了 HTTP/2 和 HTTP/3,多路复用不再是问题,头部压缩大幅减少了每个 HTTP 请求的元数据开销。结果,我们需要根据其新的利弊重新考虑合并和精灵图的使用:

  • 打包的资源可能导致不必要的数据传输:用户可能不需要特定页面上的所有资源,或根本不需要。
  • 打包的资源可能导致昂贵的缓存失效:一个组件中的单字节更新会强制完整获取整个包。
  • 打包的资源可能延迟执行:许多内容类型在完整响应传输之前无法处理和应用。
  • 打包的资源可能在构建或交付时需要额外的基础设施来生成关联的包。
  • 如果资源包含相似内容,打包的资源可能提供更好的压缩。

在实践中,虽然 HTTP/1.x 提供了对每个资源进行细粒度缓存管理的机制,但有限的并行性迫使我们打包资源。延迟获取的惩罚超过了缓存有效性降低、更频繁更昂贵的失效以及延迟执行的成本。

HTTP/2 和 HTTP/3 通过提供请求和响应多路复用支持消除了这种不幸的权衡,这意味着我们现在可以通过交付更细粒度的资源来优化应用:每个资源可以有优化的缓存策略(过期时间和重新验证令牌),并单独更新而不会使包中的其他资源失效。简而言之,HTTP/2 和 HTTP/3 使我们的应用能够更好地利用 HTTP 缓存。

也就是说,HTTP/2 和 HTTP/3 并未完全消除合并和精灵图的效用。需要牢记的一些额外考虑:

  • 包含相似数据的文件在打包时可能实现更好的压缩。
  • 每个资源请求都有一定的开销,无论是从缓存读取(I/O 请求),还是从网络获取(I/O 请求、线上元数据和服务器处理)。

对于所有应用没有单一的最优策略:交付单个大包不太可能产生最佳结果,发出数百个小资源请求也可能不是最优策略。正确的权衡将取决于内容类型、更新频率、访问模式和其他标准。为获得最佳结果,收集你自己的应用的测量数据并相应优化。


测试 HTTP/2 与 HTTP/3 服务器质量

HTTP/2 或 HTTP/3 服务器或代理的朴素实现可能 “会说” 协议,但没有良好实现的流控和请求优先级支持,它很容易产生次优性能。例如,它可能通过发送大的低优先级资源(如图像)来饱和用户的带宽,而浏览器在接收到更高优先级资源(如 HTML、CSS 或 JavaScript)之前被阻塞无法渲染页面。

使用 HTTP/2 和 HTTP/3,客户端对服务器寄予很大信任。为获得最佳性能,HTTP/2/3 客户端必须 “乐观”:它用优先级信息注释请求(参见流优先级)并尽快将它们分派给服务器;它依赖服务器使用通信的依赖关系和权重来优化每个响应的交付。一个良好优化的 HTTP 服务器一直很重要,但有了 HTTP/2 和 HTTP/3,服务器承担了以前超出范围的额外关键责任。

在测试和部署 HTTP/2 和 HTTP/3 基础设施时做好尽职调查。常见的测量服务器吞吐量和每秒请求的基准测试无法捕获这些新要求,可能无法代表你的用户在加载应用时的实际体验。


优化资源加载优先级

请求优先级的目的是允许客户端表达当容量有限时它希望服务器如何交付响应——例如,服务器可能准备好发送多个响应,但由于带宽有限,它应该优先于其他资源发送一些资源。

  • 如果服务器忽视所有优先级信息会怎样?
  • 高优先级流应该总是优先吗?
  • 是否存在不同优先级流应该交错的情况?

如果服务器忽视所有优先级信息,那么它就有造成客户端不必要处理延迟的风险——例如,通过先于更关键的 CSS 和 JavaScript 资源发送图像来阻塞浏览器渲染页面。然而,以严格的依赖顺序交付流也可能产生次优性能,因为它可能重新引入队头阻塞问题,即高优先级但慢的响应可能不必要地阻塞其他资源的交付。结果,一个良好实现的服务器应该给予高优先级流优先权,但也应该在所有高优先级流被阻塞时交错低优先级流。


迎接 HTTP/3 时代

HTTP/3 是 HTTP 协议的最新演进,基于 QUIC(Quick UDP Internet Connections)构建,QUIC 是一个基于 UDP 的传输协议,专为快速、移动和延迟敏感的互联网而设计。HTTP/3 于 2022 年由 IETF 正式标准化,现已获得主流浏览器、云平台和 Web 服务器的广泛实现。

与基于 TCP 的前代协议不同,HTTP/3 解决了 TCP 的诸多固有限制:

  • 消除队头阻塞:HTTP/2 在传输层仍受 TCP 队头阻塞影响,一个丢包会阻塞所有流。HTTP/3 的 QUIC 基于 UDP,实现了真正的流独立,一个流的丢包不会影响其他流。
  • 更快的连接建立:QUIC 将加密和握手合并,通常只需一次往返(0-RTT 或 1-RTT)即可建立安全连接,相比 TLS 1.3 仍需的握手过程进一步减少延迟。
  • 连接迁移:QUIC 使用连接 ID 而非 IP 地址标识连接,允许客户端在网络切换(如从 WiFi 到蜂窝)时无缝迁移连接,无需重新握手。

截至 2024-2025 年,HTTP/3 的普及率已快速增长,约 40% 的网站已支持 HTTP/3。对于现代 Web 应用,建议采取以下策略:

  • 渐进式启用:在服务器上启用 HTTP/3,让支持的客户端自动协商使用,同时保持对 HTTP/2 和 HTTP/1.1 的回退支持。
  • 优化 QUIC 实现:确保服务器的 QUIC 实现支持良好的流控和拥塞控制算法,如 BBR。
  • 调整资源调度:HTTP/3 的流独立性意味着可以更激进地并行请求资源,但仍需考虑无线网络的带宽和电池限制。

HTTP/3 不是对 HTTP/2 的颠覆,而是对其的增强。所有 HTTP/2 的优化原则——单一连接、消除分片、细粒度缓存——在 HTTP/3 中依然适用,且执行得更加彻底。