render blocking resources:先找出挡住首屏的那些,再决定修哪个

render blocking resources 是浏览器在画出第一个像素之前必须先下载并处理的样式表和脚本。这一章教你先把它们列出来、把真正花钱的挑出来,再用两种改法把等待挪走——附一条对任意网址都能跑的列清单命令。

改写与发布6 分钟读完1092 次阅读
render blocking resources:先找出挡住首屏的那些,再决定修哪个

render blocking resources(渲染阻塞资源)指的是浏览器在画出第一个像素之前,必须先下载并处理完的样式表和脚本。它本身不是 bug——浏览器是在保护你,不让你看到一张没样式的页面。真正的问题是把它们一视同仁。这一章给一条五步:先把它们列出来,再按实际代价排个序,最后只把真正挡住首屏的那两个挪走。

读这篇前

你只需要一个线上页面和一个终端,除了下面那条命令不用装任何东西。这一章写给已经上线、能改模板或前端代码的人;如果页面现在连改都改不动,先解决那一层。目标指标是 largest contentful paint,渲染阻塞资源是把它拖晚的最常见原因之一。想看一批真实站点在样式表上到底怎么做的,render blocking css 实测 26 个首页 是数字那一半;同一个面板里脚本那一半在 unused JavaScript 实测。

为什么一个资源会挡住首屏

浏览器要画出一张完整的页面,手里得同时有 DOM 和 CSS 对象模型。DOM 是边解析 HTML 边攒出来的。CSS 对象模型不一样,它必须等所有被判定为阻塞渲染的样式表都到齐才能建,因为一条规则能改变已经解析过的任何东西长什么样。所以 head 里的一张样式表会一直卡着首屏,直到它下载完、解析完。浏览器不会先画一版再修正,它就是等着。

普通 <script> 挡的方式又不一样。它会把 HTML 解析器本身停下来,因为脚本可能往文档里写东西。这一停,DOM 跟着停,后面那次绘制也一起停。带上 defer 或 async 的脚本,或者 type="module" 的脚本,下载时不占着解析器。两种机制,同一个肉眼可见的症状:屏幕白着,网络上还有东西在说话。

render blocking resource 不是「慢」,是「早」。要改的是它什么时候被需要,不是把它压小。

这个区别正是它值得单开一章的原因。把一张阻塞样式表压缩,几乎从来不是解法,因为等的是顺序,不是体积。解法是先判断哪些资源真的必须赶在首屏之前到,再把其余的挪开。

按这个顺序做

五步,每一步都有一个能核对的完成标志。第二步别跳,它是拦住你「优化一个根本不是问题的文件」的那一道。

  1. 先把阻塞请求列出来。对线上地址跑下面那条命令。完成标志:拿到一份样式表加 head 脚本的清单,每条后面带一个字节数。
  2. 把「必须挡」和「不必挡」分开。给每条标一句:首屏需不需要它。完成标志:「不必挡」那一列非空;对多数站来说,它还是多数。
  3. 先修样式表这一侧。首屏要用的关键规则内联进去,剩下的走不占渲染的方式加载。完成标志:head 里不再有一条指向那张延后样式表的阻塞 link。
  4. 再修脚本这一侧。把 head 里的普通脚本移到 body 末尾,或者加上 defer。完成标志:从 <head> 到首屏之间,再没有一条不带 async/defer 的脚本。
  5. 再跑一遍对比。同一条命令、同一个页面。完成标志:清单变短了,largest contentful paint 往前挪了。

第三步是最容易做错的,因为最顺手的动作就是把整张样式表内联进去。这笔交换是真的,而且有代价,下面「做错了会怎样」里会把它说清楚。

怎么找出你的 render blocking resources

最快的清单来自 Lighthouse 的 render blocking requests 审计,它会逐条打印阻塞的地址、传输大小,以及它把渲染卡了多久。就一条命令,读的是浏览器工具里打印的同一份 JSON。

npx lighthouse https://example.com/ \
  --only-audits=render-blocking-insight \
  --output=json --output-path=blocking.json --quiet

2026-10-02 拿它对 github.com 跑一次,返回 20 条阻塞请求。全部是样式表,没有一条脚本——这是清单给的第一件有用的信息:那一页的 JavaScript 已经全部让开了。20 张样式表加起来 279 KB。最大的一张主题样式表 72 KB,审计把 313 毫秒的阻塞算在它头上。

阻塞最久的不是最大的那张。一张 7.5 KB 的样式表,体积大约只有它的十分之一,却被记了 1,765 毫秒,是那张 72 KB 文件的五倍还多。

资源体积阻塞
primer-react-brand-css.module.css72,143 B313 ms
primer-4136ede8.css43,599 B313 ms
global-54ba76e9.css40,846 B157 ms
primer-react-css.module.css35,230 B157 ms
light-99f877e9.css7,574 B1,765 ms

把它读成「一个页面、抓一次」,是这条命令的演示,不是对整个站点的分数。体积那一列才是你能据以行动的,因为它每次一样;阻塞时长那一列随网络变,这一次它恰好说明——一个发现得晚、或者服务端回得慢的小文件,能比一个大文件卡得更久。单次读数不是对你站的测量。

交付物:一张用来做决定的表

「有几个阻塞资源」这个数本身没用。你要据以行动的是把两件事配起来:它赶不赶得上首屏、把它留在这里有多贵。你会遇到的阻塞资源,都落进下面四行里。

资源为什么挡怎么处理
首屏要用的样式表画之前得先有 CSSOM留着,但把它做小;这是它凭本事挣来的位置
首屏以下的样式表同一个道理,但这些规则现在还用不到延后加载,或者只把关键规则拆出来内联
head 里的普通 script停了解析器,DOM 也就停了加 defer;真的必须先跑就把它做极小
抢跑的三方标签看着小,常常不小,而且你多半管不了放到首屏之后再加载,或者干脆不放

最有用的一个习惯:先按体积把清单排一遍,然后把最大的几条念出来。一个又大、首屏又用不上的阻塞资源,才是唯一值得花一上午的那种。其余可以挑个空闲的日子再说。

做错了会怎样

下面三种错法反复出现,而且在网速快的机器上用浏览器看,三种都像进展。

  1. 把整张样式表内联进去。内联确实消掉了那次阻塞请求,首屏可以立刻出来。代价是每一份 CSS 都进了 HTML,每次导航都跟着走一遍,再也不能单独缓存。样式表大的站,等于拿一个可缓存的请求,换来每一页都甩不掉的体积。
  2. 给一个本来必须先跑的脚本加 defer。被 defer 的脚本在解析之后才跑,所有指望它早点跑完的东西现在都晚了一步,或者和页面其余部分抢跑。只有首屏不依赖的脚本,才适合 defer。
  3. 把 lab 清单当成对整站的判决。一张样式表放在快的 CDN 上、缓存又热,几乎不花什么;审计分不出这种情况和贵的那种。拿清单决定挪什么,再去看 field 数字,才知道挪完有没有换来什么。

常见问题

render blocking resources 怎么消除?

不是把它们全消掉。是把清单缩到首屏真正需要的那几条,把它们内联或预加载,其余的用 media 切换、defer、或者首屏后再加载移出关键路径。一个阻塞资源为零的页面,通常意味着全部内联了,那有它自己的代价。

预加载(preload)能解掉阻塞吗?

不能。预加载改的是浏览器多早开始抓一个文件,不改它挡不挡渲染。被预加载的阻塞样式表照样挡。预加载最有用的时候,是资源被发现得太晚,比如埋在样式表深处的一个字体。

render blocking resources 是排名因素吗?

单看它不是。它的意义在于拖晚 largest contentful paint,而 Core Web Vitals 会进 page experience 信号。Google 从没公布过「阻塞多少毫秒值多少名次」这种换算,所以把它当成一个被计入了排名的用户问题,而不是一根有明码标价的杠杆。

async 能去掉阻塞吗?

对脚本可以:async 和 defer 都让解析器继续走,脚本不再挡渲染。样式表没有这两个属性,所以 CSS 得换一种修法。经典写法是 media="print" 配一个把它改回 all 的 onload;我们自己那份 26 个首页的样式表实测里,没有一个站用它。

一个页面该有几个渲染阻塞资源?

没有目标数,谁给你一个都是在猜。有用的问题是:清单上每一条,首屏是不是真的需要。一份很短但都必要的清单,好过一份很长但都很便宜的清单,因为成本是那次等待,不是条数。

边界要说清楚:这一章讲的是浏览器的首屏绘制。一个取到 HTML 就不再渲染的爬虫,从不等待其中任何一条,所以一张挡住浏览器的样式表对它一分钱都不花。一个资源挡不挡,还取决于它在哪儿被发现,所以用脚本插进去的样式表要带 blocking="render" 才会挡——这条路径我们没测过,不会给一个没见过的结论。把阻塞清单做短,再去盯落地页上的 field 数字,正是 QueryWin 在做的事。

本文属于 QueryWin 实操手册 · 第 2 阶

render blocking resources:先找出挡住首屏的那些,再决定修哪个