本文最后更新于:2026年2月12日 凌晨
HTTP/2 多路复用到底比 HTTP/1.x 强在哪?
先聊聊 HTTP/1.x 的那些坑
HTTP/1.0:一个请求一个连接,傻到家了
每个请求都得新建 TCP 连接,请求完立马断开:
1 2 3 4 5 6 7 8 9 10 11
| 浏览器 ─────TCP 握手─────► 服务器 ◄────握手完成───── ─────请求 1──────► ◄────响应 1─────── ─────TCP 断开─────► 浏览器 ─────TCP 握手─────► 服务器 ◄────握手完成───── ─────请求 2──────► ◄────响应 2─────── ─────TCP 断开─────►
|
这啥概念?TCP 三次握手已经够慢了,如果是 HTTPS 还得加 TLS 握手,这延迟叠在一起,页面加载能急死人。
HTTP/1.1:Keep-Alive 来了,但也没好到哪去
Connection: keep-alive 让 TCP 连接能复用了:
1 2 3 4 5 6 7 8 9
| 浏览器 ─────TCP 握手─────► 服务器 ◄────握手完成───── ─────请求 1──────► ◄────响应 1─────── ─────请求 2──────► ◄────响应 2─────── ─────请求 3──────► ◄────响应 3─────── ─────TCP 断开(超时后)─────►
|
省了握手开销是挺好,但有个要命的毛病:请求只能排队等着,一个没完下一个别想走 —— 这就是臭名昭著的队头阻塞。
HTTP/1.1 管道化:试过,但失败了
想了个招,允许一口气发多个请求:
1 2 3 4 5 6 7 8
| 浏览器 ─────TCP 握手─────► 服务器 ◄────握手完成───── ─────请求 1──────► ─────请求 2──────► ─────请求 3──────► ◄────响应 1─────── ◄────响应 2─────── ◄────响应 3───────
|
听着不错对吧?但响应必须按请求顺序返回。万一请求 1 的响应慢得像蜗牛,请求 2、3 就算早准备好了也得干等着。所以现实里基本没人用这功能。
HTTP/2 多路复用:这才叫并行
核心思路
把请求/响应拆成一个个独立的流(Stream),多个流在同一条 TCP 连接上穿插着走:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| HTTP/1.1 Keep-Alive: ┌─────────────────────────────────────────┐ │ 请求 1 ───────────────────────────────► │ │ ◄────────────────────────────── 响应 1 │ │ 请求 2 ───────────────────────────────► │ │ ◄────────────────────────────── 响应 2 │ │ 请求 3 ───────────────────────────────► │ │ ◄────────────────────────────── 响应 3 │ │ │ │ 特点:排队,前一个完事才能下一个 │ └─────────────────────────────────────────┘
HTTP/2 Multiplexing: ┌─────────────────────────────────────────┐ │ 流 1: ════HEADERS════════════════════► │ │ 流 3: ════HEADERS═════════DATA═══════► │ │ 流 1: ════════════════════DATA═══════► │ │ 流 5: ════HEADERS═════════DATA═══════► │ │ 流 3: ════════════════════DATA═══════► │ │ 流 1: ◄═══════════════════DATA════════ │ │ │ │ 特点:各走各的,谁也不堵谁 │ └─────────────────────────────────────────┘
|
关键区别一览
| 特性 |
HTTP/1.1 Keep-Alive |
HTTP/2 Multiplexing |
| 连接数 |
同域名 6-8 个 |
同域名 1 个 |
| 请求方式 |
排队串行 |
真正并行 |
| 响应顺序 |
必须按顺序 |
想谁先回就谁先回 |
| 队头阻塞 |
有 |
HTTP 层解决 |
| 头部压缩 |
没有 |
HPACK 压缩 |
| 优先级控制 |
没有 |
有 |
多路复用是怎么实现的?
流(Stream)
HTTP/2 里每个请求/响应对应一个流,用流 ID 区分:
- 客户端发起的流:奇数 ID(1, 3, 5…)
- 服务器发起的流:偶数 ID(用于推送)
- 流 ID 不能复用
帧(Frame)
数据被切成一个个帧,每个帧打上流 ID 的标签:
1 2 3 4 5 6 7 8 9 10 11 12 13
| ┌──────────────┬──────────┬──────────┬──────────────┐ │ 长度(24bit) │ 类型(8bit)│ 标志(8bit)│ 流 ID(31bit) │ ├──────────────┴──────────┴──────────┴──────────────┤ │ 负载数据 │ └────────────────────────────────────────────────────┘
示例: 帧 1: {stream_id: 1, type: HEADERS, data: ...} 帧 2: {stream_id: 3, type: HEADERS, data: ...} 帧 3: {stream_id: 1, type: DATA, data: ...} 帧 4: {stream_id: 5, type: HEADERS, data: ...} 帧 5: {stream_id: 3, type: DATA, data: ...} 帧 6: {stream_id: 1, type: DATA, data: ...}
|
接收方按 stream_id 把帧重新拼成完整的请求/响应。
看图说话

这张图说明白了:
- HTTP/1.x:请求 A 卡住了,请求 B 只能等着
- HTTP/2:请求 A 卡了关我 B、C 什么事?照走不误
服务器推送:多路复用的骚操作
传统模式:等浏览器要我才给
1 2 3 4 5 6 7
| 浏览器: GET /index.html 服务器: 返回 index.html 浏览器: [解析 HTML]... 浏览器: GET /style.css 服务器: 返回 style.css 浏览器: GET /app.js 服务器: 返回 app.js
|
时间线:
1 2 3 4 5 6
| index.html: ████████░░░░░░░░░░░░ parse HTML: ████░░░░░░░░░░ style.css: ████████░░░░ app.js: ████████ ↑ 总时间
|
服务器推送模式:我猜你要啥,提前塞给你
1 2 3 4 5 6 7 8 9
| 浏览器: GET /index.html 服务器: 返回 index.html PUSH_PROMISE /style.css ← 预测推送 PUSH_PROMISE /app.js ← 预测推送 [推送 style.css 数据] [推送 app.js 数据] 浏览器: [解析 HTML]... [style.css 已经在了] [app.js 已经在了]
|
时间线:
1 2 3 4 5 6
| index.html: ████████░░░░░░░░░░░░ parse HTML: ████░░░░░░░░░░ style/css: ████████████░░░░░░░░░░ ← 并行 app.js: ██████████████████░░░░ ← 并行 ↑ 总时间(快多了)
|


推送别乱用
适合推的:
- 首页关键 CSS/JS
- 用户大概率会访问的下一页资源
别瞎推的:
- 浏览器可能已经缓存的 —— 浪费带宽
- 根本用不到的资源 —— 浪费流量
配合缓存策略来,只推那些没缓存的。
头部压缩:省下的都是钱
HTTP/1.x 的 Header 有多啰嗦:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| # 请求 1 GET /page1 HTTP/1.1 Host: example.com User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)... Accept: text/html,application/xhtml+xml... Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Accept-Encoding: gzip, deflate, br Connection: keep-alive Cookie: session=abc123; user=xyz789
GET /page2 HTTP/1.1 Host: example.com ← 重复 User-Agent: Mozilla/5.0... ← 重复 Accept: text/html... ← 重复 Accept-Language: zh-CN... ← 重复 Accept-Encoding: gzip, deflate, br ← 重复 Connection: keep-alive ← 重复 Cookie: session=abc123; user=xyz789 ← 重复
|
HTTP/2 的 HPACK 压缩:
1 2
| 请求 1: 完整发送(约 400 字节) 请求 2: 只发不一样的(约 10 字节)
|
100 个请求能省约 40KB 的 Header 传输。
性能实测
典型网页(1 个 HTML + 3 个 CSS + 5 个 JS + 10 个图片):
| 指标 |
HTTP/1.1 |
HTTP/2 |
提升 |
| TCP 连接数 |
6-8 个 |
1 个 |
-85% |
| 首屏时间 |
2.5s |
1.2s |
-52% |
| 完全加载时间 |
4.0s |
2.1s |
-47% |
| 总传输大小 |
850KB |
780KB |
-8% |
切到 HTTP/2 后,这些优化可以扔了
1. 域名分片
1 2 3 4 5 6 7
| <script src="//static1.example.com/app.js"></script> <script src="//static2.example.com/lib.js"></script>
<script src="//cdn.example.com/app.js"></script>
|
2. 文件合并
1 2 3 4 5 6 7
|
import { Button } from './components/Button'; import { Chart } from './components/Chart';
|
3. 雪碧图
1 2 3 4 5 6 7
| background: url('sprites.png'); background-position: -10px -20px;
background: url('icon-search.png');
|
4. 内联资源
1 2 3 4 5 6 7 8
|
<style></style> <script></script>
<link rel="stylesheet" href="/critical.css">
|
一句话总结
| 维度 |
HTTP/1.x Keep-Alive |
HTTP/2 Multiplexing |
| 连接复用 |
✅ 能复用 TCP |
✅ 也能复用 |
| 请求并行 |
❌ 串行排队 |
✅ 真·并行 |
| 队头阻塞 |
❌ 存在 |
✅ 解决 |
| 头部开销 |
❌ 大 |
✅ 压缩后小 |
| 推送能力 |
❌ 没有 |
✅ 服务器主动推 |
| 优先级 |
❌ 没有 |
✅ 有 |
多路复用是 HTTP/2 最硬核的改进,它从根本上改变了 Web 资源的传输方式。搞懂它和 HTTP/1.x 的本质区别,写代码的时候心里才有底。