文章

HTTP/1.1与HTTP/2协议对比深入解析

HTTP/2 用多路复用、二进制分帧和 HPACK 头部压缩解决了 HTTP/1.1 的应用层队头阻塞, 但 TCP 层的队头阻塞仍在,这正是 HTTP/3(QUIC)要解决的。面试必考对比。

HTTP/1.1与HTTP/2协议对比深入解析

一句话概括

HTTP/2 是 HTTP 协议自 1999 年 HTTP/1.1 之后的首次重大升级,不是重写而是”换一种更高效的传输方式”。它要解决的问题很明确:现代网页动辄七八十个请求,HTTP/1.1 在性能上已经扛不住了。

HTTP/2 的核心就四件事:二进制分帧(协议从文本变成二进制)、多路复用(一个 TCP 连接里并发传多个请求)、HPACK 头部压缩(砍掉重复头部)、服务器推送(服务端主动塞资源)。前三个直接干掉了 HTTP/1.1 的”队头阻塞 + 头部冗余 + 域名分片”三座大山。

但划重点:HTTP/2 只解决了”应用层”的队头阻塞,TCP 层该堵还堵,于是又有了基于 UDP 的 HTTP/3(QUIC)。这条演进线(1.1 → 2 → 3)是面试里最爱追着问的。

核心知识点

1. HTTP/1.1 的三处硬伤

HTTP/1.1 设计时网页才几十 KB、几个请求,放到今天全面崩盘:

  • 队头阻塞(HOL Blocking):同一连接同一时刻只能处理一个请求,后面的必须排队。即使开了 keep-alive,也是”攒够一个再发下一个”,不是并行。
  • 头部冗余:每次请求都重复带 User-Agent、Cookie、Accept 等,七八十个请求就是几十 KB 的重复字节,比数据本身还大。
  • 域名分片(Domain Sharding):浏览器对每个域名最多约 6 个连接,为了并发,开发者被迫把资源拆到 cdn1/2/3.example.com 多个子域,纯属 workaround。
1
2
3
4
5
// 这些"上古优化"在 HTTP/2 下反而有害:
// ❌ CSS Sprites / 雪碧图:一张大图改一个图标全量失效
// ❌ 文件合并成一个 all.js:改一行整个文件缓存失效
// ❌ 域名分片:多开连接反而和复用背道而驰
// ✅ HTTP/2 下正确做法:按需拆包,每个文件独立缓存

2. 二进制分帧 + 多路复用

HTTP/2 把请求/响应拆成更小的帧(Frame),每个请求是一个流(Stream),多个流的帧在同一个 TCP 连接里交错传输,接收方按流 ID 重组回来。

1
2
3
4
5
6
客户端                                      服务器
  │── 流1 HEADERS GET /index.html ───────→│
  │── 流2 HEADERS GET /app.js ──────────→│
  │←─ 流2 DATA app.js(第1块) ───────────│  ← 谁先就绪谁先回,不再排队
  │←─ 流1 DATA index.html(第1块) ──────│
  │←─ 流2 DATA app.js(第2块) ───────────│

关键收益:

1
2
HTTP/1.1:请求1 → 等响应1 → 请求2 → 等响应2   (串行)
HTTP/2  :请求1、2、3 同时发,响应按帧交错回来 (真正的并发)

帧结构(了解即可,面试常让画):Length(24bit) | Type(8bit) | Flags(8bit) | R + StreamID(31bit) | Payload。常见帧类型 HEADERS、DATA、SETTINGS、PUSH_PROMISE、GOAWAY。

3. HPACK 头部压缩

HTTP/2 用 HPACK 算法压缩头部,原理是静态字典 + 动态字典 + 哈夫曼编码:

  • 静态字典::method、:path、Cookie 这类高频头部直接映射成 1 个字节的索引
  • 动态字典:本次连接里出现过的头部(比如你的某个长 Cookie)也记下来,后续只发索引
  • 结果:头部从 600–800 字节压到 30–50 字节,压缩率约 85–95%
1
2
3
// 前端怎么看自己走的是哪个协议
const nav = performance.getEntriesByType('navigation')[0];
console.log(nav?.nextHopProtocol); // "h2" = HTTP/2,"http/1.1" = HTTP/1.1

4. 残余队头阻塞:HTTP/2 没解决干净

这是面试最高频的陷阱题:”HTTP/2 真的完全解决了队头阻塞吗?”

答案:没完全解决。 它解决的是应用层(请求级别)的排队,但所有流共用一个 TCP 连接——只要有一个 TCP 包丢了,整个连接上的所有流都得停下来等它重传。

1
2
3
TCP 连接上同时跑着 流1/流2/流3
        💥 流1 的某个包丢了,要重传
流2、流3 即使数据已经到了,也得在接收缓冲区里干等这个重传

这就是 HTTP/3 诞生的动机:把传输层从 TCP 换成 UDP + QUIC,在 UDP 上自己实现可靠传输,并且队头阻塞隔离到”流”级别——某个流丢包只影响它自己,其他流照跑。

5. 服务器推送:为什么现在不推荐了

HTTP/2 允许服务端在客户端请求 HTML 时,主动 PUSH_PROMISE 把 CSS/JS 一起推过去,省一次 RTT。听起来很美,但现实坑很多:

  • 推的资源客户端可能已经缓存了,白推
  • 推送时机、优先级难控制,常常推了不用的东西
  • 主流 CDN 对推送支持参差

所以现在更推荐用 103 Early Hints 或 <link rel="preload"> / preconnect 让浏览器自己去拿,控制权交回前端:

1
2
3
4
<head>
  <link rel="preload" href="/critical.css" as="style">
  <link rel="preconnect" href="https://api.example.com">
</head>
1
2
3
4
5
6
7
8
# Nginx 开启 HTTP/2(注意:浏览器只支持基于 TLS 的 h2,必须 https)
server {
    listen 443 ssl;
    http2 on;                       # Nginx 1.25+ 写法;老版本用 listen 443 ssl http2;
    server_name example.com;
    ssl_certificate     /path/cert.pem;
    ssl_certificate_key /path/key.pem;
}

其实你每天都在用

  • 打开任意现代网站:Chrome DevTools → Network → 勾选 “Protocol” 列,看到 h2 就是 HTTP/2,你天天在用只是没注意
  • 首屏并发加载 css/js/img:以前靠域名分片硬凑并发,现在一个连接全搞定
  • performance.getEntriesByType('navigation'):排查性能时看 nextHopProtocol 确认是否走了 2/3
  • Vite/Rollup 打包策略:HTTP/2 下不再盲目合并,而是按路由拆 chunk,改一个组件只失效一个文件
  • CDN/大厂默认开启:Cloudflare、阿里云 CDN 等默认就是 h2,你部署上去自动受益
  • 看 B 站/抖音网页版卡顿:某些卡可能是 TCP 层丢包导致整连接 HOL blocking,这正是 HTTP/3 要救的

常见误解(FAQ)

❌ 误区一:”HTTP/2 把队头阻塞彻底解决了”

错。它只解决了应用层(请求级别的串行排队),TCP 层的队头阻塞原封不动——一个包丢失,同一连接上所有流都要等重传。彻底解决要靠 HTTP/3(QUIC/UDP)。

❌ 误区二:”HTTP/2 不需要 HTTPS,和 1.1 一样能明文跑”

规范层面有明文的 h2c,但所有主流浏览器都只实现基于 TLS 的 h2。也就是说线上用 HTTP/2,几乎必然要配 HTTPS,这点和 HTTP/1.1 能走明文不同。

❌ 误区三:”升级 HTTP/2 后,以前那些性能优化(雪碧图、合并文件)更该做了”

正好反过来。多路复用让”少请求数”这件事不再重要,域名分片、雪碧图、all-in-one 合并反而有害(缓存粒度变粗、复用率下降)。HTTP/2 下应该拆得更细、按文件独立缓存。

❌ 误区四:”服务器推送(Server Push)是 HTTP/2 的杀手锏,应该用上”

现实是坑多于利,推错/重复推、CDN 兼容性差,业界已普遍弃用,改用 103 Early Hints 或 preload/preconnect 让浏览器自主决定。

一句话总结

HTTP/1.1 → 2 的升级,本质是用二进制分帧 + 多路复用 + HPACK 把”一个连接串行排队”变成”一个连接并发交错”,干掉了应用层队头阻塞和头部冗余;但 TCP 层的坑还在,于是有了 HTTP/3(QUIC/UDP)——记住这条线,面试关于协议的追问就稳了。

本文由作者按照 CC BY 4.0 进行授权