4. HTTP/2多路复用与流优先级工程调优
HTTP/2 的多路复用把多个请求压缩到同一条 TCP 连接上并发传输,解决了 HTTP/1.1 队头阻塞带来的连接数膨胀问题。但多路复用本身并不保证“重要的资源先到”。当一条连接上同时存在 HTML、关键 CSS、首屏图片、埋点脚本、预加载资源等几十个流时,它们会竞争同一份带宽和 TCP 发送窗口。如果浏览器和服务器都不做优先级调度,先发出的流往往先占满带宽,关键渲染资源反而被排在后面,首屏时间被拖长。流优先级(Stream Priority)就是用来在共享连接上表达“谁更重要、谁依赖谁”的机制。
优先级树:依赖关系与权重的双重表达
HTTP/2 的优先级模型是一棵依赖树。每个流可以声明自己依赖另一个流(父流),并给出 1~256 的权重。语义是:父流是子流的前置依赖,子流应当在父流完成后才被优先分配资源;同一父节点下的兄弟流按权重比例分配可用带宽。权重不是绝对带宽,而是相对比例——两个权重 16 和 32 的兄弟流,在竞争时大致按 1:2 分配。
需要特别区分两个概念:依赖(dependency)决定“谁先谁后”的顺序约束,权重(weight)决定“同层之间分多少”。只调权重不调依赖,只能影响兄弟间的比例;只调依赖不调权重,则顺序正确但同层分配可能失衡。工程调优通常两者一起用。
工程调优:从“能并发”到“关键资源先到”
浏览器在解析 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 连接。观察要点:
- 首屏关键 CSS、字体是否显示为 Highest/High,且排在图片之前开始传输。
- 埋点、统计脚本是否被降为 Low/Lowest,其 Waterfall 起始时间是否明显晚于关键资源。
- 同一连接上多个流的 Waterfall 是否出现“交错推进”——这是多路复用的典型特征,而非 HTTP/1.1 的串行排队。
- 若关键资源仍被阻塞,检查是否有
preload缺失或服务器未发送 PRIORITY 帧。
用 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(埋点),则优先级调度生效。
常见资源竞争问题与处理
| 现象 | 可能原因 | 处理方向 |
|---|---|---|
| 首屏图片晚于埋点加载 | 埋点脚本权重过高或未降权 | 设置 fetchpriority="low",服务器降权 |
| 关键 CSS 被图片阻塞 | CSS 未 preload,依赖树未建立 | preload + PRIORITY 帧声明高权重 |
| 多流仍串行推进 | 服务器忽略优先级或 TCP 窗口过小 | 检查服务器实现,调大初始窗口 |
| 字体闪烁严重 | 字体流权重低,被其他资源挤占 | 提升字体流权重,使用 font-display |
调优的闭环是:先明确首屏关键资源清单,再通过 HTML 提示与服务器 PRIORITY 帧建立依赖树和权重,最后用 DevTools 看优先级列、用 Wireshark 看帧级调度,确认关键资源确实先到。若服务器不支持优先级,应优先考虑升级服务器或迁移到 HTTP/3,而不是在应用层反复调整。