如果你刚把站点从HTTP/1.1升级到HTTP/2,大概率会想尝试服务端推送(Server Push)这个特性。它最典型的应用场景是:客户端请求一个HTML页面,服务器在返回HTML的同时,主动把页面依赖的CSS、JavaScript甚至图片一起推送到客户端缓存中。这样浏览器解析到对应资源时不需要再发请求,直接命中缓存,首屏渲染时间明显缩短。Nginx从1.13.9版本开始支持HTTP/2推送,核心指令就是http2_push,但很多开发者配置后要么没有效果,要么推送到不该推的资源,问题多半出在“列出条目”这一步。

推送条目的“列出”有两种路径:一种是在Nginx配置中使用http2_push指令逐个写死需要推送的资源URI;另一种是依靠上游应用返回的Link响应头,由Nginx自动解析并推送。两种方式各有适用场景,但无论哪种方式,都需要透彻理解URI的匹配规则和Nginx内部对推送请求的处理机制,否则配置很容易变成“摆设”。下面先从推送的基础模型说起,再分别讲解两种列出条目的方法。
HTTP/2 Push在Nginx中的触发模型
服务器推送的本质是服务端在响应主请求的同时,并发地发起若干个“虚拟请求”。这些虚拟请求的URI由服务端指定,响应内容会被客户端存储到对应的缓存条目中。当浏览器真正需要这些资源时,直接从缓存取用,无需再发出真实的网络请求。在HTTP/1.1时代没有这个能力,因为一个连接同一时刻只能处理一个请求响应,而HTTP/2的多路复用允许在一个TCP连接上并行发送多个响应。
Nginx对推送的支持通过http2_push指令落地,该指令只能出现在location块中。它的参数是一个URI,表示这个主请求返回时额外要推送哪些资源。例如在location /下配置了http2_push /css/main.css;,那么每当用户访问这个location匹配的路径(比如首页/),Nginx都会自动向客户端推送/css/main.css这个资源。这里的关键点是推送请求的URI是相对于同一个虚拟主机的,如果CSS文件实际放在/static/css/main.css,参数就必须写成完整路径,不能用相对路径。
另一个重要细节是:Nginx不会真的去请求磁盘上的文件然后推送出去,而是模拟一个子请求去获取该URI对应的内容。如果这个URI在Nginx里没有一个匹配的location来提供响应,推送就会失败。比如刚配置了http2_push /css/main.css,但/css/main.css这个路径没有对应的location或者被重定向到其他位置,Nginx在子请求阶段只能返回404,最终推送不会成功。因此在列出条目之前,建议先确认这些资源URI在Nginx里都是可以正常访问的。
使用http2_push指令手动列出推送条目
手动列出条目的方式最直观,适合静态资源URL固定且数量不多的场景。配置示例如下:
server {
listen 443 ssl http2;
server_name ipipp.com;
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
root /var/www/html;
location / {
http2_push /css/main.css;
http2_push /js/app.js;
http2_push /images/logo.png;
}
location /css/ { ... }
location /js/ { ... }
location /images/ { ... }
}
上面的配置中,当用户访问/对应的首页时,Nginx会并行推三个资源。注意这里的资源路径必须带上开头的斜杠,并且不能包含查询字符串。如果原页面里引用的资源带版本号如/css/main.css?v=20240301,推送时不能写查询参数,因为HTTP/2推送的缓存键是基于URI的,查询串会导致推送条目和真实请求不一致。实践中通常在构建阶段就把版本号嵌入文件名,比如main.20240301.css,这样可以避免缓存同步问题。
这种方式的缺点也很明显:页面一旦引入新的依赖资源,需要手动修改Nginx配置并重载服务。对于频繁迭代的前端项目来说,维护成本较高。另外如果同一个资源被多个location推送,可能会造成重复推送。Nginx本身没有去重机制,同一个资源被推送多次会给客户端带来额外开销,好在浏览器会忽略重复推送,但网络带宽还是被浪费了。因此手动列出条目更适合资源很少且变化不频繁的静态站点。
借助Link响应头自动发现推送条目
更灵活的做法是由上游应用(比如后端程序或反向代理的源服务器)在响应头里添加Link字段,然后Nginx使用http2_push_preload指令开启自动推送。这种方式下,列出条目的工作从Nginx配置转移到了应用代码中。例如在PHP后端里可以这样写:
header('Link: </css/main.css>; rel=preload; as=style', false);
header('Link: </js/app.js>; rel=preload; as=script', false);
对应的Nginx配置只需要在location中加上一行http2_push_preload on;:
location / {
http2_push_preload on;
proxy_pass http://backend;
}
当Nginx从后端收到包含Link头的响应时,会读取其中rel=preload的条目,并根据as参数判断资源类型,然后自动发起子请求获取这些资源并推送。每个Link头可以包含多个逗号分隔的条目,或者应用发送多个Link响应头。需要注意的是rel=preload必须存在,如果只有rel=stylesheet这样的普通关系,Nginx不会进行推送。
自动发现机制的好处是运维人员和前端开发可以解耦:前端在构建时生成带有preload链接的HTML,后端读取这些信息写入响应头,Nginx只负责执行推送。但这里面有一个坑:如果后端返回的Link头部里的资源URI与Nginx实际处理该资源的location不匹配,推送依然会失败。尤其当后端和Nginx位于不同机器,由反向代理转发时,URI需要从客户端角度出发书写,而不是后端内部路径。例如后端代码中Link: ; rel=preload; as=style,但用户访问的域名下静态文件路径是/assets/main.css,此时推送就会404。
推送条目的最佳实践与常见误区
很多开发者在配置好推送后会发现首屏性能不升反降,通常是推送了太多资源或者推送了浏览器已经缓存的资源。HTTP/2 Push没有内置的缓存感知机制,也就是说即使客户端已经在协商阶段宣告自己有某个资源的缓存(通过Session Ticket或Cache Digest扩展,但主流浏览器都不支持),服务器也可能盲目推送。因此一个常见的方案是只对首屏关键资源进行推送,比如渲染必需的第一屏CSS和JS,而将次要资源留给浏览器按需加载。通过控制推送条目的数量,可以避免抢占主文档的带宽。
另一个常见误区是认为http2_push和http2_push_preload可以同时使用并叠加。其实两者优先级不同:当http2_push_preload on开启时,Nginx会优先推送Link头中的条目,同时还会执行location块中显式声明的http2_push条目。如果两种方式指向同一个资源,会造成重复推送。建议在生产环境只用其中一种方式,不要混用,除非你明确清楚每个条目的来源。
还有一个容易被忽略的点:推送请求本质上也是一个HTTP/2流,它同样会消耗连接窗口。如果推送大文件(比如几个MB的图片),会立刻占满TCP拥塞窗口,反而拖慢主文档的传输。实践中通常只推送小体积且关键的文件,比如几十KB的CSS或JS。对于图片、字体等大资源,使用preload提示让浏览器并行加载就足够了,并不需要服务端推送。
排查推送是否成功,可以打开浏览器开发者工具的网络面板,在时间线中查看被推送的资源,通常会标记为“Push”或显示极短的请求时间。也可以使用nghttp命令测试:nghttp -ans https://ipipp.com/,输出中会列出所有被推送的流和对应的URI。

