Skip to content

HTTP/1.1 瓶颈与队头阻塞

基于 HTTP 现代标准 · 核于 2026-06

速查

  • 持久连接(keep-alive):HTTP/1.1 默认开启,一条 TCP 连接可复用于多个请求,省去重复握手;但连接即使空闲也占用服务器资源。
  • 致命短板:一条持久连接上请求仍是串行的——必须等上一个响应回来才能发下一个,这就是「等响应」式的队头阻塞。
  • 管线化(pipelining)失败:允许不等响应就连发请求,但 RFC 强制响应必须按请求顺序返回;第一个响应慢,后面全被卡住 → 应用层队头阻塞(HOL Blocking)。叠加坏代理、难实现,现代浏览器默认关闭
  • 应用层 HOL ≠ TCP 层 HOL:本页讲的是「响应必须排队返回」的应用层阻塞;HTTP/2 多路复用解决了它,但 TCP 丢包导致的传输层 HOL 仍在,要等 HTTP/3 的 QUIC 才彻底解决。
  • 并发连接上限:浏览器对每个域名(origin)通常只开 6 条 TCP 连接(早期仅 2~3),超出会排队;过多连接还可能触发服务端 DoS 防护。
  • 头部冗余:每个请求都重复携带大量头部 + Cookie,且无压缩,小资源多时头部开销甚至超过正文。
  • 前端被迫的 hack:域名分片(domain sharding)、雪碧图(CSS sprites)、文件合并(concatenation)、资源内联(inlining/data URI)——本质都在「凑请求数」「绕连接上限」。
  • hack 的代价:破坏缓存粒度(改一处全量失效)、强制提前加载用不到的资源、内联资源无法独立缓存、分片增加 DNS/握手/TLS 成本。
  • HTTP/2 反噬:这些 hack 在 HTTP/2 下不再需要,甚至有害——域名分片会被「连接合并(connection coalescing)」抵消,合并/内联反而拖慢缓存命中。
  • 结论:连接模型 + 头部模型双重瓶颈,催生了 HTTP/2 的二进制分帧、多路复用与 HPACK 头部压缩。

一、回顾:持久连接(keep-alive)解决了什么

最早的 HTTP/1.0,每个请求都要新建一条 TCP 连接、用完即关。在 RTT 动辄几十毫秒、还叠加 TLS 握手的今天,一个页面几十上百个资源,光握手就能拖垮加载速度。

HTTP/1.1 把持久连接设为默认:一条 TCP 连接建立后保持打开,可被后续多个请求复用,省去反复握手的开销。

http
GET /index.html HTTP/1.1
Host: example.com
Connection: keep-alive

持久连接的代价

连接即使空闲也会占用服务器的文件描述符与内存。高并发下大量空闲连接会耗尽资源,因此服务端常配置 Keep-Alive 超时与最大请求数来回收。这也解释了为什么浏览器不能对单域名无限开连接。

但持久连接只省了「握手」,没解决根本问题:在一条连接上,请求依然是串行的——客户端必须等上一个响应完整回来,才能发下一个请求。下载第 N 个资源前要苦等前 N-1 个,这正是队头阻塞的雏形。

二、管线化(pipelining):一个失败的修补

为了打破串行等待,HTTP/1.1 引入了管线化:客户端可以在一条持久连接上连续发出多个请求,而不必等待各自的响应

客户端 →  请求1  请求2  请求3   (一口气发出,不等响应)
服务端 ←  响应1  响应2  响应3   (但必须按这个顺序返回!)

理论上能省掉请求间的等待时间,但它从设计上就埋了雷,最终沦为废弃特性、浏览器默认关闭

2.1 为什么失败:响应必须按序返回

协议规定服务器必须按请求到达的顺序返回响应。于是只要队首的「请求 1」对应一个慢响应(比如服务端要查数据库、生成大文件),即便「响应 2」「响应 3」早已就绪,也只能干等在队列里,无法提前送达客户端。

这就是「应用层队头阻塞」(HOL Blocking)

一个慢响应会卡住它后面所有已经准备好的响应——阻塞发生在 HTTP 应用层的响应队列上,与底层 TCP 是否丢包无关。这是本页的核心概念,请务必与下文的 TCP 层 HOL 区分开。

2.2 其他致命伤

  • 坏代理普遍存在:当年大量中间代理对管线化处理有 bug,导致响应错乱、行为诡异,前端无法预判和排查。
  • 难以正确实现:资源大小、RTT、带宽都会影响管线化收益;调度不当会让重要资源被堵在不重要资源后面。
  • 收益微薄:综合下来大多数场景只有边际提升。

结论:管线化被 HTTP/2 的**多路复用(multiplexing)**取代——后者用独立的流(stream)让响应可以乱序交错返回,从根上消灭应用层 HOL。

应用层 HOL vs TCP 层 HOL(一句话点明区别)

  • 应用层 HOL(本页主角):HTTP 协议要求响应按序返回造成的阻塞。HTTP/2 多路复用即可解决。
  • TCP 层 HOL(第 5 页 QUIC 详述):即使 HTTP/2 多路复用,一旦底层 TCP 丢一个包,该连接上所有流都得停下来等重传。这是 TCP 字节流的本质限制,HTTP/2 解决不了,要靠 HTTP/3 的 QUIC(基于 UDP、按流独立恢复)才能根治。

三、并发连接数限制:每域名通常 6 条

既然单连接串行、管线化又不可用,浏览器只能用「多开连接」来凑并发:对同一个域名(origin)并行开启多条 TCP 连接,各自独立发请求。

但这个数量有硬上限——主流浏览器对每个域名通常只允许 6 条并发连接(早期仅 2~3 条)。

时期每域名并发连接数
早期浏览器2 ~ 3
现代浏览器(主流)6

为什么要限制?

  • 保护服务器:若每个客户端都无限开连接,服务端连接数会爆炸,过多并发还可能触发 DoS 防护被判为攻击。
  • 保护网络与客户端自身:每条连接都有内存、握手、拥塞控制状态的成本,盲目增多反而劣化整体吞吐。

后果很直接:一个现代页面动辄上百个资源,6 条连接根本不够分,多出来的请求只能排队等待空闲连接,加载被拖长。这就逼出了下一节的各种「绕过限制」的 hack。

四、头部冗余:重复且无压缩

HTTP/1.x 的第二大瓶颈在头部(Headers)

  • 每个请求都重复携带几乎相同的头部——User-AgentAcceptReferer,尤其是整坨 Cookie,每次请求原样再发一遍。
  • 完全没有压缩:HTTP/1.x 头部是纯文本明文传输,没有任何压缩机制(正文可以 gzip,头部不行)。

对一个页面里几十上百个小资源(图标、小图、小脚本)来说,重复头部的体积甚至可能超过资源正文本身,在上行带宽受限的移动网络下尤其浪费。

Cookie 是重灾区

站点 Cookie 一旦较大,每个同域请求(哪怕只是取一张几百字节的小图)都要把整个 Cookie 重发一遍。这正是后来 HTTP/2 用 HPACK 头部压缩重点要解决的问题(见第 4 页)。

五、前端被迫发明的 hack 与它们的代价

面对「连接数只有 6、单连接串行、头部还冗余」的三重枷锁,前端社区发明了一系列减少请求数 / 绕开连接上限的优化套路。它们在 HTTP/1.1 时代确实有效,但每一个都带着副作用。

Hack做法目的主要代价 / 副作用
域名分片
(domain sharding)
把资源拆到 img1.x.comimg2.x.com 等多个子域突破「单域名 6 连接」上限(3 域 × 6 = 18 并发)额外 DNS 解析 + TCP 握手 + TLS 握手成本;连接数失控反而拖累;HTTP/2 下被「连接合并」抵消甚至有害
雪碧图
(CSS sprites)
把许多小图标拼成一张大图,用 background-position 裁切把 N 个图片请求合并成 1 个改一个小图标要重切整图、缓存全失效;只为显示一个图标却要下载整张大图;维护繁琐
文件合并
(concatenation)
把多个 JS/CSS 打包成一个大文件减少请求数破坏缓存粒度:改一行代码整个 bundle 哈希变化、用户重下全部;首屏被迫加载本不需要的代码
资源内联
(inlining)
data: URI 把小图/CSS 直接嵌进 HTML 或 CSS干脆省掉这个请求内联资源无法被独立缓存,每次随页面重复下载;base64 编码体积膨胀约 33%;撑大主文档延后首屏

5.1 共同的本质与代价

这些 hack 看似各异,本质都是**「用打包/合并去对冲请求数与连接数的限制」**,因此都共享同一类代价:

  • 牺牲缓存效率:合并/内联让「细粒度、各自独立缓存」变成「一损俱损」——任意一处改动导致整体缓存失效、全量重下。
  • 强制提前加载(eager loading):把暂时用不到的资源也一起塞进首屏包,浪费带宽、拖慢首屏。
  • 增加复杂度与构建负担:雪碧图坐标、分片域名、打包策略都要工具链维护,出错难排查。

在 HTTP/2 下这些 hack 会「反噬」

HTTP/2 用一条连接上的多路复用就能高效并发,请求数不再是瓶颈。此时:

  • 域名分片有害:HTTP/2 推崇单连接,多数实现还会用**连接合并(connection coalescing)**把分片域名重新归并;硬分片只剩额外握手成本。
  • 合并/内联/雪碧图反而拖慢缓存命中:HTTP/2 下更应保持资源细粒度、可独立缓存

所以升级 HTTP/2 时,要主动拆掉这些 HTTP/1.1 时代的优化(详见第 6 页性能实践)。

六、这些瓶颈如何催生 HTTP/2

把上面的问题归一下类,HTTP/1.1 的瓶颈集中在两个模型上:

  • 连接模型烂:单连接串行 + 管线化失败(应用层 HOL)→ 只能靠开 6 连接 + 域名分片硬撑并发。
  • 头部模型烂:头部重复且无压缩 → 小资源开销被头部吃掉。

HTTP/2 正是冲着这两点去的:

  • 二进制分帧 + 多路复用:一条 TCP 连接上用独立的流并发收发、响应可乱序交错 → 干掉应用层队头阻塞,连接数限制和域名分片随之失去意义(第 3 页)。
  • HPACK 头部压缩:压缩并复用重复头部 → 解决头部冗余(第 4 页)。

但 HTTP/2 没解决一切

HTTP/2 消灭了应用层 HOL,却没碰传输层(TCP) HOL:一旦底层 TCP 丢包,整条连接的所有流都得停等重传。这个遗留问题最终由 HTTP/3 的 QUIC 解决(第 5 页)。

小结

HTTP/1.1 的持久连接省下了反复握手,但没改变「单连接上请求串行、响应必须按序返回」的本质;管线化想打破串行却因应用层队头阻塞(一个慢响应卡死后面所有响应)和坏代理等问题而废弃。为凑并发,浏览器对每个域名只开约 6 条连接,前端被迫祭出域名分片、雪碧图、文件合并、资源内联等 hack——它们在当年有效,却普遍以破坏缓存粒度、强制提前加载、增加复杂度为代价,且在 HTTP/2 下反而有害。叠加头部重复无压缩的浪费,这套连接模型与头部模型的双重瓶颈,正是 HTTP/2 用多路复用与 HPACK 要根除的目标。需记牢:本页的队头阻塞是应用层的(HTTP/2 可解),它不同于 TCP 丢包导致的传输层队头阻塞(要等 HTTP/3 的 QUIC)。


上一页:HTTP 版本演进史 | 下一页:HTTP/2 二进制分帧与多路复用