1. 从一个慢页面的琐碎等待说起做 Web 开发的人大概都遇到过这种场景页面里只有十几个资源文件但打开速度就是慢得离谱——首屏要等两三秒图片一张张蹦出来控制台里全是 pending 状态的请求。早期我们总习惯把锅甩给服务器带宽后来一次次排查才发现瓶颈往往不在带宽而在 HTTP 协议本身。HTTP/1.1 已经服役了二十多年它在设计之初就没想过今天的网页会包含几十上百个请求。每个请求一个连接、串行排队、请求头重复传输这些机制在文本时代够用但在富媒体时代成了实实在在的拖累。于是 2015 年HTTP/2 正式定稿核心思路不是去修补 HTTP/1.1 的边边角角而是重新设计了一套数据交换方式。这篇文章就把 HTTP/2 从头到尾拆开讲一遍。我会先梳理 HTTP/1.1 的几个核心痛点然后逐一对照 HTTP/2 的解决思路——二进制分帧、多路复用、头部压缩、服务端推送、优先级和流量控制最后聊一些实测下来的配置要点和容易踩的坑。无论你是在自己调 Nginx还是纯粹想搞明白浏览器控制台里那串协议版本是什么意思这篇都能给你一个完整的脉络。2. HTTP/1.1 的几个憋屈之处HTTP/2 的改造起点2.1 队头阻塞一条车道上的串行等待HTTP/1.1 的持久连接解决了反复握手的问题但同一个连接上的请求仍然是严格串行的。一个请求发出去必须等服务端返回完整响应才能发下一个请求。如果一个响应因为数据量太大或者后端处理慢而卡住后面排队的请求统统得等着。这就是队头阻塞Head-of-Line Blocking。浏览器拿它没什么好办法只能开多个连接来并行加载。所以 HTTP/1.1 时代浏览器默认对同一域名开 6 个左右的 TCP 连接就是为了用“多开几条车道”的方式绕过串行瓶颈。但连接数终归是有限的。一旦某个页面需要的资源超过连接数剩下的请求还是要排队。更麻烦的是TCP 的慢启动机制会让每个新连接都从较低的传输速率开始——连接开得越多整体效率反而可能更差。注意这里说的队头阻塞是应用层的请求响应阻塞。到了 HTTP/2 里它并没有被彻底消灭而是被转移到了 TCP 传输层。这个区别后面要单独讲。2.2 头部冗余每次请求都在重复发送同样的内容HTTP/1.1 的请求头是纯文本格式每次请求都要完整带上。以 Cookie 和 User-Agent 为例一个典型的请求光头部就有几百字节。如果页面有 100 个请求这些重复信息就要传输好几万字节而且基本都是同样的内容。我们做个粗略估算假设一个请求头平均 400 字节100 个请求就是 40KB。这 40KB 里可能 80% 是重复数据。对于高并发场景这部分开销会直接体现在带宽成本和响应延迟上。HTTP/1.1 时代唯一的优化手段是尽可能减少请求数——合并 CSS、精灵图本质上都是被这个缺陷逼出来的。2.3 只能客户端主动服务端很被动HTTP/1.1 的请求-响应模型里服务端永远处于被动地位。客户端不发请求服务端就没有办法把数据主动推给客户端。这带来一个很现实的问题网页引用的 CSS、JS、图片浏览器必须先解析 HTML发现资源引用再逐个发起请求。这个“发现”过程每多一个 RTT往返时延就多一次等待。在移动网络环境下RTT 可能达到几十甚至上百毫秒累积起来的浪费相当可观。实现层面虽然有 WebSocket 等补充方案但那属于另一个协议和 HTTP 语义并不完全匹配。所以 HTTP/2 的设计目标非常明确解决连接利用率低、头部开销大、服务端被动这三个主要矛盾。下面我从最底层的分帧机制开始逐个展开讲。3. 二进制分帧和多路复用HTTP/2 最核心的一层3.1 从“请求报文”到“流上的帧”HTTP/1.1 的请求和响应是完整的、不可分割的报文。TCP 传输时它就是一连串字节接收方要靠 Content-Length 或者 chunked 编码来把报文边界切分出来。HTTP/2 改变了这里的底层模型它把整个通信过程拆成了两个层面。上层是语义层仍然保留请求、响应、头部、数据这些概念下层则新增了一个二进制分帧层。每个请求和响应都被拆成一个个更小的单元叫做“帧”Frame。所有帧都共享同一个 TCP 连接。帧里记录了自己的类型、所属流 ID、长度等信息。这样做的好处是同一个连接上可以同时交错传输多个请求和响应的片段接收方根据流 ID 把它们重新组装。这个过程就像物流分拣不同客户请求的包裹打散后放进同一辆货车到达目的地后再按客户重新分类配送。3.2 多路复用如何解决队头阻塞因为有了二进制分帧HTTP/2 可以在一个 TCP 连接上同时发起任意数量的请求。浏览器不再需要为每个域名开 6 个连接而是可以只用一个连接把所有请求交错发送到服务端。服务端的响应也以帧为单位交错返回谁先处理完谁先走不需要等前面的响应完成。这就是多路复用Multiplexing。它直接解决了 HTTP/1.1 的队头阻塞问题。具体来说并发能力从“按连接数排队”变成了“按单连接内流并发”页面加载的逻辑彻底变了资源的加载顺序不再受限于请求发起顺序而是由到达时间和权重共同决定。我在本地做过一个简单的压测用 HTTP/1.1 加载一个包含 80 个小资源的页面开 6 个连接耗时大约 2.8 秒换成 HTTP/2 后同样的资源只需要 0.9 秒左右。当然不同环境结果差异很大但量级的差距是明确的。3.3 为什么帧必须拆得足够小帧的设计对多路复用至关重要。如果帧太大一个流的帧就会长时间占住连接其他流只能等它传输完如果帧太小又会有太多的帧头开销。HTTP/2 默认的帧大小上限是 16384 字节16KB但允许通过 SETTINGS 帧协商调整。小帧交错传输的好处在于任何单个流的数据都不会独占总线太久其他流可以及时插入帧传输。这样既保证了公平性也让高优先级的关键请求比如 HTML 文档本身能更快被传输。这种机制在弱网环境下尤其重要——RTT 高的时候如果没有交错机制光排队等待就能让用户感受到明显卡顿。4. 头部压缩与 HPACK把重复开销摊平4.1 静态表、动态表和 Huffman 编码HTTP/2 中新增了头部压缩机制使用的是 HPACK 算法。它和普通 gzip 压缩不同不只是对内容做一次压缩而是利用请求头本身的高度重复性建立了一套“索引表”。HPACK 有两张表。静态表中预置了常用的头部字段比如 :method: GET、:scheme: https、:path: / 这些每个字段都有固定编号。发送方只需要发一个数字接收方就能还原出对应的完整头部。动态表则是连接建立后动态维护的两次相同请求之间重复出现的头部字段会被记录下来后续用索引号替代。再加上 Huffman 编码对头部字符串做编码压缩效果叠加起来相当可观。实际场景中带 Cookie 的请求头可能从 400 字节压缩到 20 字节左右压缩率能达到 80% 到 90%。注意动态表是基于连接状态的长连接里才能真正发挥威力。如果频繁断连重连动态表每次都要重建压缩效果会打折扣。这也是 HTTP/2 中连接复用的价值被一再强调的原因之一。4.2 头部压缩带来的性能变化头部压缩的效果在移动端尤其明显。一个页面如果包含几十个请求头部占用的流量过去可能占整体流量的 10% 甚至更多。在 2G/3G 时代这是不可忽视的成本即便在 4G/5G 和 Wi-Fi 环境下减少这部分开销也能间接改善首屏加载速度。我测试过某个 API 服务平均请求头 500 字节左右开启 HTTP/2 后抓包看到同样的请求头实际只传输了 60 字节左右。如果是高频轮询接口这个差距乘以请求次数节省的流量相当可观。4.3 不要把 HPACK 和 gzip 混为一谈有些教程把头部压缩说成“对请求头做 gzip”这是不准确的。gzip 是无状态压缩HPACK 是带状态的有损编码丢失的是冗余信息不是数据本身。两者原理和使用场景都不同。理解这个区别有助于排查问题——如果你发现某个环境里请求头没有被压缩或者动态表命中率很低先检查连接是否复用而不是怀疑压缩算法出了故障。5. 流优先级、依赖关系和流量控制5.1 流优先级让重要资源先到多路复用把并发请求放在同一条连接上但并不是所有请求都同等重要。浏览器希望 HTML 文档和首屏关键 CSS 能尽快到达而图片、统计脚本可以慢慢来。HTTP/2 提供了优先级机制每个流可以指定权重也可以声明对其他流的依赖关系。权重用来表示同级别流之间的相对重要性范围是 1 到 256默认 16。依赖关系则可以让某个流依赖另一个流——父流完成后子流才进入资源分配。浏览器一般会自动设置默认优先级开发者也可以通过响应头或者服务端配置来调整。这里有一个实际经验默认优先级下HTML、CSS、JS 会优先于图片但这不一定符合所有场景。比如某些电商活动页的首屏是图片海报把图片优先级调高一点反而能更快完成首屏绘制。HTTP/2 里可以通过服务端主动调整依赖树但实际应用不多大多数时候浏览器默认值就够用了。5.2 流量控制单流级别的滑动窗口HTTP/2 在 TCP 的流量控制之上又增加了一层流级别的流量控制。它可以针对单个流设置窗口大小防止某个慢消费者拖慢整个连接上的其他流。比如说浏览器正在下载一个大图但 JS 引擎还没有处理完前一个脚本HTTP/2 的流控制可以限制大图流的传输速率同时让其他流的帧正常通过。默认窗口大小是 65535 字节可以通过 SETTINGS_INITIAL_WINDOW_SIZE 修改。这个参数需要客户端和服务端配合调整如果设置得太小高带宽环境下会出现吞吐量上不去的情况如果太大又可能在弱网环境造成缓冲区堆积。我建议一般场景使用默认值除非有明确的性能测试数据支撑再调整。5.3 优先级和流量控制在实践中如何观察浏览器开发者工具的 Performance 面板可以直观看到资源加载的优先级标记。不同浏览器对 HTTP/2 优先级实现有差异比如旧版浏览器可能完全不支持依赖树而是把所有资源按顺序轮流分配。在服务端Nginx 的 http2 模块支持通过http2_push或http2_max_concurrent_streams等指令控制行为和并发上限。并发流上限默认是 128如果实际并发超过这个值超出的请求会被缓冲。实测中对于资源密集型页面这个上限一般够用但如果你有跑大量小请求的场景可以适当调大。6. 服务端推送理想很丰满实践需谨慎6.1 什么是服务端推送HTTP/2 的服务端推送Server Push允许服务端在客户端还没有发起请求时主动把资源发过去。典型的场景是浏览器请求 index.html服务端知道这个页面一定会引用 style.css于是直接把这个 CSS 推给浏览器省去浏览器解析 HTML 后再发现资源、再发起请求的额外 RTT。从原理上看服务端推送确实继承了 HTTP/1.1 时代“资源内联”和“预加载”的思路但又更进一步——它不改变资源本身的 URL 和缓存语义浏览器收到推送的资源后可以正常走缓存逻辑。6.2 为什么推送没有被大范围使用理论上很美但实际落地时坑很多。最大的问题是服务端并不知道浏览器缓存里有没有这个资源。如果你推了一个浏览器已经缓存的 CSS那就是白白浪费带宽和连接资源。更麻烦的是推送需要占用并发流浏览器还要额外消耗内存接收这些数据。HTTP/2 推送在实际中的使用率一直不高。Chrome 甚至在其 106 版本移除了对 HTTP/2 服务端推送的支持理由是使用率太低、收益不明显。如果你在考虑使用推送我建议先做好资源是否已缓存的判断或者干脆忽略这个特性把精力放在多路复用和头部压缩上。6.3 替代方案预加载与 103 Early Hints如果不使用服务端推送又想达到“提前发现资源”的效果更稳妥的方向是用Link响应头配合preload或modulepreload。这些是纯浏览器的预加载机制相比推送它们仍然需要浏览器主动发起请求但能提前发起降低网络等待。还有一个方案是 103 Early Hints 状态码——服务端在处理正式响应之前先发一个包含Link头的早期响应让浏览器提前去下载关键资源。实测下来在高 RTT 场景下Early Hints 对小部分关键资源的加速效果非常明显而且不存在推送的资源浪费问题。这个方案的实现成本低值得在性能优化清单里占一个位置。7. 从 HTTP/1.1 切换到 HTTP/2 的实操配置7.1 先确认你的站点和资源准备好了没有在开启 HTTP/2 之前有几个前提条件需要确认。HTTP/2 在浏览器端的实现要求必须基于 TLS所以准备工作首先是 HTTPS 证书。如果没有证书HTTP/2 在浏览器里基本不会启用明文 HTTP/2 协议在主流浏览器里是不支持的。同时要知道HTTP/2 的多路复用并不会自动解决所有性能问题。资源数量多但没有做合理压缩的站点切换到 HTTP/2 后可能只会提升一点点速度因为真正的瓶颈可能在图片体积或后端响应时间上。所以建议在切换前先做一次资源审计缩小图片、合并小文件、压缩文本资源然后再开启 HTTP/2。7.2 Nginx 开启 HTTP/2如果你用 Nginx开启 HTTP/2 只需在 listen 指令后加上http2参数。下面是一个最小配置server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/ssl/example.com.crt; ssl_certificate_key /etc/ssl/example.com.key; root /var/www/html; index index.html; }重新加载配置后就可以通过 curl 验证curl -I --http2 https://example.com如果响应头里的HTTP/2 200出现说明 HTTP/2 已经生效。注意旧版本 Nginx1.25.0 之前的配置语法不太一样用的是listen 443 ssl; http2 on;这种写法。7.3 如何在浏览器里验证最简单的方法是打开浏览器开发者工具切到 Network 面板右键表格标题栏勾选 Protocol 列。如果加载的资源显示h2那就代表走的是 HTTP/2如果显示http/1.1说明没有生效或者被降级了。再有就是用 curl 看响应头里的alt-svc或者直接抓包。如果你做服务端性能优化建议搭配nghttp这类工具来做 HTTP/2 专项请求测试它可以打印每个请求的实际耗时还能看出并发流的使用情况。7.4 常见的配置陷阱开启 HTTP/2 后会遇到一个隐蔽的坑Nginx 默认的http2_max_concurrent_streams是 128但如果你同时开启了很多长连接比如 WebSocket 或者 SSE它们会占用流资源导致后续请求被阻塞。这类问题的症状是连接明明建好了请求却一直 pending。另外如果你使用了 443 端口同时做 WebSocket 和 HTTP/2 流量需要特别注意 Nginx 的http2_push_preload以及proxy_http_version配置。代理场景下如果 Nginx 和后端之间用的是 HTTP/1.1尽管客户端看到的是 HTTP/2端到端的收益也会被削弱。要最大程度发挥 HTTP/2 的性能建议在反向代理到后端时也开启 HTTP/2部分后端服务支持否则多路复用和头部压缩只覆盖了前半段链路。8. 常见问题与排查技巧实录8.1 问题记录与解决方案速查我自己维护的某服务在切换 HTTP/2 后遇到过几个典型问题整理成表格供参考问题现象可能原因处理方式浏览器加载资源仍显示http/1.1证书无效或未配置 TLS、Nginx 版本不支持检查证书有效性升级 Nginx重启后验证HTTP/2 请求长时间 pending并发流被占满、后端响应过慢调大http2_max_concurrent_streams优化后端耗时资源加载顺序混乱依赖关系设置不合理、浏览器忽略了权重在服务端调整响应头优先级或用Link头预加载关键资源抓包发现头部压缩率很低动态表命中率低、连接没有持续复用检查是否频繁建立新连接增加 Keep-Alive 时长服务端推送导致流量浪费推送了客户端已有缓存资源停用推送改用preload预加载或 Early Hints8.2 一个真实调试场景为什么我的站点只有部分资源走了 h2某次测试中站点证书和 Nginx 配置都没问题但个别资源在 Network 面板显示的是http/1.1。排查后发现这部分资源是通过一个旧的 JSONP 接口加载的接口侧在响应头里带了Connection: close。这个响应头会让浏览器关闭连接导致该请求无法复用 HTTP/2 连接直接降级为 HTTP/1.1。后来在服务端清理了这类响应头整体资源全部回到 h2。这个案例说明HTTP/2 的生效不仅取决于前端服务器还受业务接口、代理层、甚至中间设备影响。8.3 使用第三方 CDN 时注意协议协商如果你的站点前置了 CDN还需要确认 CDN 是否支持 HTTP/2 回源以及客户端接入。部分老的 CDN 节点只支持 HTTP/1.1导致整体链路降级。可以用 curl 指定--http2分别测边缘节点和源站对比两边返回的不同版本就能定位问题节点是哪一段。另外如果你启用了多种协议HTTP/1.1 和 HTTP/2要注意 ALPN 协议的协商结果。Nginx 中可以通过配置ssl_alpn控制协议的优先级确保 HTTP/2 排在 HTTP/1.1 前面。默认情况下 Nginx 会自动优先选择 HTTP/2但自定义配置时容易把顺序搞反。8.4 排查清单遇到 HTTP/2 相关的问题我一般按这个顺序排查先用浏览器 Network 面板确认协议列显示的内容。再用 curl 带-I --http2验证边缘节点响应头。检查 Nginxerror.log和access.log看有无协议相关的报错。确认证书链完整签发的域名和访问域名一致。检查是否存在代理层强制降级或者响应头里带了Connection: close。最后才考虑是不是并发流、流量控制等参数配置问题。这个顺序能帮你快速把问题范围缩小先确认协议是否生效再看配置层面然后分析业务链路而不是一上来就调整高级参数。9. 后续还能怎么玩HTTP/2 与未来协议的衔接HTTP/2 解决了 HTTP/1.1 时代最明显的几个问题但它并非终点。多路复用虽然解决了应用层的队头阻塞但 TCP 层的队头阻塞依然存在——当一个 TCP 数据包丢失时后续所有流都会被阻塞即使它们属于不同的 HTTP/2 流。这也是 HTTP/3 转而基于 UDP 实现 QUIC 的根本原因。HTTP/3 的 QUIC 协议把流和数据包层面的独立性做到了极致实现了真正的“无队头阻塞”传输。对于弱网和高丢包环境HTTP/3 的表现会比 HTTP/2 更稳定。不过 HTTP/3 的部署依赖 UDP 443 端口以及 CDN 全面改造短时间内 HTTP/2 依然是 Web 性能优化的主流方案。对于现在的项目把 HTTP/2 用好——开启 TLS 1.3、配置合理的缓存策略、合理使用预加载——已经能在普遍场景下获得稳定的性能提升。等 HTTP/3 生态更成熟再考虑逐步切换这是比较稳妥的演进路径。10. 最后分享一点实测心得从 HTTP/1.1 切到 HTTP/2我给好几个站点做过改造整体感受是收益最明显的是那些请求数量多但单个资源不大的页面比如博客、资讯站、H5 活动页而如果站点本身已经把资源合并到极致、请求数很少HTTP/2 的提升幅度就不那么突出。一个容易被忽视的细节是HTTP/2 的缓冲区配置对性能影响很大。Nginx 的http2_recv_buffer_size默认是 128KB如果单个请求的响应头比较大、或者请求体较大缓冲区满了会导致请求处理变慢。实测中把它调大到 256KB 后部分慢请求明显减少。另外HTTP/2 的动态表和流量控制参数都有讲究但我不建议一开始就追求极限调优。先把协议开启、资源压缩做好、缓存配好性能通常已经比 HTTP/1.1 提升了一个档次。之后如果还有瓶颈再针对你的具体场景逐步调整优先级和窗口大小。如果你在切换 HTTP/2 的过程中遇到什么奇怪的问题按上面的排查思路一步步来大概率能定位到原因。我也希望听完这些踩坑记录后你能少走一些弯路。
阅读完成 · 觉得有帮助?