4. HTTP/2多路复用与流优先级工程调优

HTTP/2 的多路复用把多个请求压缩到同一条 TCP 连接上并发传输,解决了 HTTP/1.1 队头阻塞带来的连接数膨胀问题。但多路复用本身并不保证“重要的资源先到”。当一条连接上同时存在 HTML、关键 CSS、首屏图片、埋点脚本、预加载资源等几十个流时,它们会竞争同一份带宽和 TCP 发送窗口。如果浏览器和服务器都不做优先级调度,先发出的流往往先占满带宽,关键渲染资源反而被排在后面,首屏时间被拖长。流优先级(Stream Priority)就是用来在共享连接上表达“谁更重要、谁依赖谁”的机制。

优先级树:依赖关系与权重的双重表达

HTTP/2 的优先级模型是一棵依赖树。每个流可以声明自己依赖另一个流(父流),并给出 1~256 的权重。语义是:父流是子流的前置依赖,子流应当在父流完成后才被优先分配资源;同一父节点下的兄弟流按权重比例分配可用带宽。权重不是绝对带宽,而是相对比例——两个权重 16 和 32 的兄弟流,在竞争时大致按 1:2 分配。

需要特别区分两个概念:依赖(dependency)决定“谁先谁后”的顺序约束,权重(weight)决定“同层之间分多少”。只调权重不调依赖,只能影响兄弟间的比例;只调依赖不调权重,则顺序正确但同层分配可能失衡。工程调优通常两者一起用。

HTTP/2 流依赖树与权重分配示意 父流为前置依赖,兄弟流按权重比例共享带宽 流 1 · HTML 文档 weight 256(根) 流 3 · 关键 CSS weight 200 流 5 · 首屏图片 weight 100 流 7 · 埋点脚本 weight 20 流 9 · 字体 weight 150 流 11 · 图标 weight 50 同层带宽分配(示意) CSS 200/320 图片 100/320 埋点 20/320 权重仅决定兄弟流之间的相对比例,不保证绝对带宽;依赖关系决定先后顺序 关键 CSS 与字体应置于高权重分支,埋点等非关键流应降权或延后

工程调优:从“能并发”到“关键资源先到”

浏览器在解析 HTML 时会自动为发现的资源建立优先级提示(Chrome 内部映射为 VeryHigh/High/Medium/Low 等),但自动推断并不总是符合业务预期。例如通过 <script> 动态注入的埋点、A/B 实验脚本,浏览器可能给到 Medium,而它们对首屏毫无价值。工程上可以从三个层面干预:

  • HTML 层:用 fetchpriority="high" 提升首屏图片/LCP 资源,用 fetchpriority="low" 压低非关键脚本;对关键 CSS 使用 rel="preload" as="style"。
  • 服务器层:在 HTTP/2 响应中发送 PRIORITY 帧或 PRIORITY_UPDATE 帧,显式声明依赖与权重。Nginx 可通过 http2_push 配合优先级,Envoy 支持 stream_dependencies 配置。
  • 应用层:把非关键请求延迟到 load 事件之后,或使用 requestIdleCallback 发起,从源头减少竞争。

下面是一段 Nginx 中显式设置流优先级的示例配置(示例参数,非实测结果):

# nginx.conf 片段:为关键资源声明高权重依赖
http {
    server {
        listen 443 ssl http2;

        location /assets/critical.css {
            # 依赖根流,权重 200,确保优先于图片与脚本
            http2_priority "root=0 weight=200";
            add_header Cache-Control "public, max-age=31536000, immutable";
        }

        location /assets/hero.webp {
            http2_priority "root=0 weight=100";
        }

        location /analytics.js {
            # 低权重,避免与首屏资源争抢
            http2_priority "root=0 weight=10";
        }
    }
}

用 Chrome DevTools 验证优先级效果

Chrome DevTools 的 Network 面板是验证优先级最直接的工具。打开 Network,右键表头勾选 Priority 列,即可看到每个请求的优先级(Highest/High/Medium/Low/Lowest)。同时勾选 Connection ID 列,可以确认多个请求是否复用同一条 HTTP/2 连接。观察要点:

  1. 首屏关键 CSS、字体是否显示为 Highest/High,且排在图片之前开始传输。
  2. 埋点、统计脚本是否被降为 Low/Lowest,其 Waterfall 起始时间是否明显晚于关键资源。
  3. 同一连接上多个流的 Waterfall 是否出现“交错推进”——这是多路复用的典型特征,而非 HTTP/1.1 的串行排队。
  4. 若关键资源仍被阻塞,检查是否有 preload 缺失或服务器未发送 PRIORITY 帧。
在 DevTools 中把网络限速设为 “Slow 4G”,优先级差异会被放大,更容易观察关键资源是否真正抢到了带宽。

用 Wireshark 抓包确认帧级行为

DevTools 展示的是浏览器视角的优先级,要确认服务器是否真正按优先级调度,需要看 HTTP/2 帧。用 Wireshark 抓取 443 端口流量,过滤表达式:

tcp.port == 443 && http2

重点观察三类帧:

  • HEADERS 帧:携带 :path、:method,确认请求对应的流 ID。
  • PRIORITY 帧:包含 E(exclusive)、Stream Dependency、Weight 字段,验证依赖树是否按预期建立。
  • DATA 帧:观察不同流 ID 的 DATA 帧交错顺序与长度,判断服务器是否把更多窗口给了高权重流。

例如在 Wireshark 中展开某个 PRIORITY 帧,若看到 Stream Dependency: 1、Weight: 200,说明该流确实依赖根流 1 且权重为 200,与配置一致。若 DATA 帧显示流 3(CSS)的字节数明显多于流 7(埋点),则优先级调度生效。

HTTP/2 优先级是“建议”而非强制。部分服务器(如早期 Nginx)会忽略 PRIORITY 帧,此时需通过服务器配置或改用 HTTP/3 的优先级方案。验证时务必以实际抓包结果为准,不能只看浏览器 Priority 列。

常见资源竞争问题与处理

现象可能原因处理方向
首屏图片晚于埋点加载埋点脚本权重过高或未降权设置 fetchpriority="low",服务器降权
关键 CSS 被图片阻塞CSS 未 preload,依赖树未建立preload + PRIORITY 帧声明高权重
多流仍串行推进服务器忽略优先级或 TCP 窗口过小检查服务器实现,调大初始窗口
字体闪烁严重字体流权重低,被其他资源挤占提升字体流权重,使用 font-display

调优的闭环是:先明确首屏关键资源清单,再通过 HTML 提示与服务器 PRIORITY 帧建立依赖树和权重,最后用 DevTools 看优先级列、用 Wireshark 看帧级调度,确认关键资源确实先到。若服务器不支持优先级,应优先考虑升级服务器或迁移到 HTTP/3,而不是在应用层反复调整。