HTTP/2 于 2015 年发布,是 HTTP 协议自 1999 年 HTTP/1.1 以来的首次重大更新。本文将详细介绍 HTTP/2 相比 HTTP/1.1 在性能上的改进,以及这些改进背后的原理。
一、HTTP/1.1 的性能瓶颈
1.1 队头阻塞(Head-of-Line Blocking)
HTTP/1.1 要求同一连接上的请求必须按顺序处理:
1 2 3
| 连接 1: |--请求A--|--请求B--|--请求C--| ^ 等待 A 完成才能处理 B
|
如果请求 A 的响应很慢,后面的请求 B、C 都要等待,这就是队头阻塞。
实际影响:
- 一个大文件阻塞整个连接
- 慢 SQL 查询阻塞后续请求
- 页面加载时间增加
1.2 并发限制
为了解决队头阻塞,浏览器会建立多个 TCP 连接:
1 2 3
| 连接 1: |--请求A--|--请求D--| 连接 2: |--请求B--|--请求E--| 连接 3: |--请求C--|--请求F--|
|
但浏览器对同一域名的并发连接数有限制(通常 6-8 个):
| 浏览器 |
同域名最大连接数 |
| Chrome |
6 |
| Firefox |
6 |
| Safari |
6 |
| Edge |
6 |
问题:
- 现代网页平均 80+ 个资源请求
- 只有 6 个并发连接远远不够
- 每个连接都有 TCP 握手开销
1.3 头部冗余
HTTP/1.1 每次请求都要发送完整的头部:
1 2 3 4 5 6 7 8
| GET /style.css HTTP/1.1 Host: example.com User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36... Accept: text/css,*/*;q=0.1 Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8 Accept-Encoding: gzip, deflate, br Cookie: session=abc123; user_id=456; preferences=dark_mode... Connection: keep-alive
|
问题:
- 典型请求头部 500-800 字节
- 大部分头部是重复的(User-Agent、Cookie 等)
- 100 个请求 = 50-80 KB 仅用于头部
1.4 文本协议效率低
HTTP/1.1 是文本协议,解析效率低:
1 2 3 4
| 需要逐字符扫描寻找: - \r\n (行结束) - : (键值分隔) - 空格 (分隔符)
|
1.5 HTTP/1.1 优化手段(Workarounds)
为了绕过这些限制,开发者不得不使用各种技巧:
| 技巧 |
说明 |
缺点 |
| 域名分片 |
将资源分散到多个子域名 |
增加 DNS 查询和 TCP 连接 |
| 资源合并 |
将多个 JS/CSS 合并成一个 |
缓存效率降低 |
| 雪碧图 |
将多个图标合并成一张图 |
维护困难 |
| 内联资源 |
将小资源内联到 HTML |
无法缓存 |
这些都是反模式,增加了开发复杂度。
二、HTTP/2 的性能改进
2.1 二进制分帧
HTTP/2 将消息分解为二进制帧传输:
1 2 3 4 5 6 7 8 9 10 11
| HTTP/1.1 (文本): GET /index.html HTTP/1.1\r\n Host: example.com\r\n \r\n
HTTP/2 (二进制帧): +-------+-------+---------------+ | Length| Type | Flags |Stream | <- 9 字节固定头部 +-------+-------+---------------+ | Frame Payload | +-------------------------------+
|
性能提升:
- 解析更快(固定位置读取 vs 字符扫描)
- 更紧凑(无冗余空格和分隔符)
- 便于多路复用
2.2 多路复用(Multiplexing)
HTTP/2 最重要的改进:单连接并行处理多个请求。
1 2 3 4 5 6
| HTTP/1.1 (串行): 连接1: |====请求A====|====请求B====|====请求C====|
HTTP/2 (多路复用): 连接1: |A帧|B帧|A帧|C帧|B帧|A帧|C帧|B帧|C帧| └─请求A交错──┘ └─B交错─┘ └─C─┘
|
原理:
- 每个请求分配一个 Stream ID
- 不同请求的帧可以交错发送
- 接收方根据 Stream ID 重组
1 2 3 4 5 6
| Stream 1 (请求A): [HEADERS] [DATA] [DATA] Stream 3 (请求B): [HEADERS] [DATA] Stream 5 (请求C): [HEADERS] [DATA] [DATA] [DATA]
实际传输顺序: [H:1][H:3][D:1][H:5][D:3][D:1][D:5][D:5][D:5]
|
性能提升:
- 消除应用层队头阻塞
- 减少 TCP 连接数(1 个连接足够)
- 更好地利用带宽
实际对比
加载 100 个资源:
| 方式 |
TCP 连接数 |
总耗时 |
| HTTP/1.1(无并发) |
1 |
100 × RTT |
| HTTP/1.1(6 并发) |
6 |
~17 × RTT |
| HTTP/2(多路复用) |
1 |
~1-2 × RTT |
2.3 头部压缩(HPACK)
HTTP/2 使用 HPACK 算法压缩头部:
静态表
61 个预定义的常用头部:
| 索引 |
头部名 |
头部值 |
| 1 |
:authority |
|
| 2 |
:method |
GET |
| 3 |
:method |
POST |
| 4 |
:path |
/ |
| 5 |
:path |
/index.html |
| … |
… |
… |
发送 GET / 只需 2 字节(索引 2 和 4)而不是完整文本。
动态表
连接期间累积的头部,可用索引引用:
1 2 3 4 5
| 第一次请求: Cookie: session=abc123 <- 完整发送,加入动态表(索引 62)
第二次请求: 62 <- 只发送索引,1 字节
|
哈夫曼编码
对字符串值进行哈夫曼压缩:
1 2
| 原始: "www.example.com" (15 字节) 压缩后: 12 字节 (节省 20%)
|
压缩效果
| 场景 |
HTTP/1.1 头部大小 |
HTTP/2 头部大小 |
压缩率 |
| 首次请求 |
800 字节 |
200 字节 |
75% |
| 后续请求 |
800 字节 |
20 字节 |
97% |
100 个请求的头部总大小:
- HTTP/1.1: 80 KB
- HTTP/2: ~3 KB
2.4 服务器推送(Server Push)
服务器可以主动推送资源,无需客户端请求:
1 2 3 4 5 6 7 8 9 10 11 12 13
| HTTP/1.1: 客户端: GET /index.html 服务器: 返回 index.html 客户端: (解析 HTML,发现需要 style.css) 客户端: GET /style.css <- 额外的往返 服务器: 返回 style.css
HTTP/2 Server Push: 客户端: GET /index.html 服务器: 返回 index.html 服务器: PUSH_PROMISE (style.css) <- 主动推送 服务器: 返回 style.css 无需额外往返!
|
流程图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 客户端 服务器 | | | HEADERS (GET /index.html) | |------------------------------------>| | | | PUSH_PROMISE (style.css, stream 2) | |<------------------------------------| | HEADERS (200 OK, stream 1) | |<------------------------------------| | DATA (index.html, stream 1) | |<------------------------------------| | HEADERS (200 OK, stream 2) | |<------------------------------------| | DATA (style.css, stream 2) | |<------------------------------------| | | 客户端收到 HTML 时,CSS 已经在缓存中
|
注意:Server Push 在实践中使用较少,HTTP/3 已经弃用此功能。
2.5 流优先级(Stream Priority)
HTTP/2 允许客户端指定请求的优先级:
1 2 3 4 5 6 7 8 9
| 高优先级: HTML, CSS (渲染关键路径) 中优先级: JavaScript 低优先级: 图片, 字体
服务器按优先级分配带宽: HTML: ████████████████████ (权重 256) CSS: ████████████████ (权重 200) JS: ████████ (权重 100) 图片: ████ (权重 50)
|
依赖树:
1 2 3 4 5 6 7
| 根 / \ HTML CSS | JS / \ img1 img2
|
JS 依赖 HTML,图片依赖 JS,确保关键资源优先加载。
2.6 流量控制(Flow Control)
HTTP/2 提供连接级和流级的流量控制:
1 2 3 4 5 6 7
| +------------------+ | 连接级窗口 | <- 整个连接的流量限制 +------------------+ | | | +----+ +----+ +----+ |流1 | |流2 | |流3 | <- 每个流独立的窗口 +----+ +----+ +----+
|
作用:
- 防止快速发送方压垮慢速接收方
- 允许接收方控制内存使用
- 避免单个流占用所有带宽
三、性能对比测试
3.1 页面加载时间
测试场景:加载包含 100 个资源的页面
| 网络条件 |
HTTP/1.1 |
HTTP/2 |
提升 |
| 高速(低延迟) |
2.1s |
1.2s |
43% |
| 4G(中等延迟) |
4.5s |
2.3s |
49% |
| 3G(高延迟) |
12s |
5.8s |
52% |
延迟越高,HTTP/2 优势越明显(因为减少了往返次数)。
3.2 连接数对比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| HTTP/1.1 加载页面: ┌────────────────────────────────────────────┐ │ 域名: example.com │ │ 连接1 ───────────────── │ │ 连接2 ───────────────── │ │ 连接3 ───────────────── │ │ 连接4 ───────────────── │ │ 连接5 ───────────────── │ │ 连接6 ───────────────── │ │ 域名: cdn.example.com │ │ 连接7 ───────────────── │ │ 连接8 ───────────────── │ │ ... │ │ 总计: 12+ 个 TCP 连接 │ └────────────────────────────────────────────┘
HTTP/2 加载页面: ┌────────────────────────────────────────────┐ │ 域名: example.com │ │ 连接1 ═════════════════ (所有请求复用) │ │ 域名: cdn.example.com │ │ 连接2 ═════════════════ │ │ 总计: 2 个 TCP 连接 │ └────────────────────────────────────────────┘
|
3.3 带宽利用率
1 2 3 4 5 6 7 8 9 10
| HTTP/1.1 (6 连接): 连接1: ███░░░███░░░███░░░ (利用率 ~50%) 连接2: ░░███░░░███░░░███ 连接3: ███░░░░░░███░░░░░░ ... 大量空闲时间等待响应
HTTP/2 (1 连接): 连接1: █████████████████████ (利用率 ~95%) 持续传输,无等待
|
四、HTTP/2 的局限性
4.1 TCP 队头阻塞
HTTP/2 解决了应用层队头阻塞,但 TCP 层队头阻塞仍然存在:
1 2 3 4 5 6 7
| TCP 层: 包1 [ACK] -> 包2 [丢失] -> 包3 [等待] -> 包4 [等待] ^ 丢包导致后续包都被阻塞
HTTP/2 多路复用在此时受影响: 流1数据 [等待] 流2数据 [等待] 流3数据 [等待]
|
这是 HTTP/3 使用 QUIC(基于 UDP)的原因。
4.2 服务器推送的问题
- 可能推送客户端已缓存的资源(浪费带宽)
- 推送时机难以把握
- 浏览器支持不一致
4.3 优先级实现差异
不同服务器对优先级的实现不同,效果参差不齐。
五、迁移建议
5.1 何时使用 HTTP/2
推荐使用 HTTP/2 的场景:
- 资源数量多的页面
- 高延迟网络环境
- 移动端应用
- API 服务(多个小请求)
5.2 迁移注意事项
之前的优化可能变成反优化:
| HTTP/1.1 优化 |
HTTP/2 建议 |
| 域名分片 |
移除,使用单域名 |
| 资源合并 |
拆分为小文件,利用缓存 |
| 雪碧图 |
使用单独图标,按需加载 |
| 内联资源 |
拆分为独立文件 |
5.3 服务器配置
Nginx 启用 HTTP/2:
1 2 3 4 5 6 7
| server { listen 443 ssl http2; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem;
}
|
六、总结
6.1 核心改进对比
| 特性 |
HTTP/1.1 |
HTTP/2 |
| 协议格式 |
文本 |
二进制 |
| 多路复用 |
不支持 |
支持 |
| 头部压缩 |
无 |
HPACK |
| 服务器推送 |
无 |
支持 |
| 流优先级 |
无 |
支持 |
| 连接数 |
多个(6-8/域名) |
单个 |
| 队头阻塞 |
严重 |
应用层无 |
6.2 性能提升来源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ┌─────────────────────────────────────────────────────────┐ │ HTTP/2 性能提升 │ ├─────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ 多路复用 │ │ 头部压缩 │ │ 二进制帧 │ │ │ ├─────────────┤ ├─────────────┤ ├─────────────┤ │ │ │ 消除队头阻塞 │ │ 减少 95% │ │ 更快解析 │ │ │ │ 减少连接数 │ │ 头部大小 │ │ 更紧凑 │ │ │ │ 降低延迟 │ │ 节省带宽 │ │ 便于复用 │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ │ │ │ v v v │ │ ┌─────────────────────────────────────────────────┐ │ │ │ 页面加载速度提升 40-60%,尤其高延迟网络 │ │ │ └─────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────┘
|
6.3 一句话总结
HTTP/2 通过多路复用消除队头阻塞,通过HPACK压缩头部,通过二进制分帧提升解析效率,使得单个 TCP 连接即可高效传输所有资源,大幅提升了 Web 性能。
参考资料