async defer 实测:27 个首页 625 个外链脚本,73 个两样都没写

async defer 这道题有第三个答案:两个都不写。27 个首页 625 个外链脚本里,73 个既没 async 也没 defer,解析器要停下来等它们,其中 22 个还在 head 里。

改写与发布6 分钟读完1949 次阅读
async defer 实测:27 个首页 625 个外链脚本,73 个两样都没写

实测 · 2026-08-26 · 27 个首页 · 单次抓取 · 只看原始 HTML

样本 / 口径:2026-08-15 起一直在用的那 30 个站,2026-08-26 各抓一次,浏览器 UA,不渲染。读的是原始 HTML 里每一个 <script>srctype,以及 asyncdefer 两个属性在不在。

async defer 这道题其实有第三个答案,而且没人是故意选它的:两个都不写。27 个首页一共 1,356 个 script 元素,其中 625 个是外链。这 625 个里 187 个写了 async、362 个写了 defer、150 个是模块,剩下 73 个什么都没写 —— 解析器会停在那儿,等它取回来并跑完。这 73 个里有 22 个在 <head> 里。

怎么测的

每个站一次 HTTP 请求,不渲染、不重试。纳入规则跑数前写死:最终状态 200、解压后正文不少于 10,000 字节、正文里含 <body。30 个站里 3 个没过(stackoverflow.commedium.com 返回 403,www.reddit.com 是 8,393 字节空壳),纳入 27 个。

两个属性一律按「属性在不在」判,不按值判。这条规矩是上一批踩出来的:按值读布尔属性,当时差点发出一条假结论。另外,一个脚本只有在同时满足「有 src」「两个属性都没有」「是经典脚本」时才计入阻塞 —— 模块天生就是延后执行的,算进去数字会虚高。

# 自己按同一个口径数一遍
curl -sL --compressed -A 'Mozilla/5.0' https://example.com/ \
  | grep -o '<script[^>]*src=[^>]*>' \
  | grep -v -E 'async|defer|type="module"' | wc -l

本篇最大的局限在这里:我们没测这些脚本有没有真的让某个访客多等了。阻塞是标记层的性质,真实耗时取决于网络、缓存和文件大小,这三样一个都没采。

async defer 各自改变了什么

两个属性都能解除阻塞。差别在于脚本什么时候跑、以及会不会保序,这才是决定你该用哪个的地方。

写法解析器何时执行保序
只有 src停下立刻
async继续取回就跑
defer继续解析完
type="module"继续解析完

MDN 的 script 元素参考页,2026-08-26 读到的原文把默认行为说得很直白:「if a script element does not include type="module", async, or defer, then it blocks parsing, not rendering.」同一页还写着「only script elements in the document's <head> can possibly block rendering」,所以我们把在 head 里的单独数了一列。

73 个阻塞脚本,四个站占了 55 个

这个数分布得很不均。27 个首页里 14 个一条阻塞脚本都没有,13 个至少有一条,而这 13 个里的四个占了 73 条中的 55 条。

站点外链阻塞在 head
slack.com16164
webflow.com18142
techcrunch.com20135
discord.com18123
arstechnica.com861
另外 8 个站各 1 到 30 到 2

Slack 是最干净的例子:它 16 个外链脚本全是裸的。落在 head 里的那 4 个包括一个同意管理脚本的引导文件和一个营销包,解析器还没走到页面正文,就先为一个第三方请求停住了。另外三个站的形状一样:标签管理器、一份 jQuery、一个同意管理脚本。

有构建步骤的站不一样。nextjs.orgvercel.comfigma.comsupabase.com 各只有一条,而且每一条都是框架自己发出来、本来就要先跑的东西:其中两个是 polyfill 包,另外两个是同名的构建 chunk。一条是有意为之,和十六条随手来的不是一回事。

四个站在同一个标签上写了两个属性

17 个脚本同时写了 asyncdefertheverge.com 8 个、bbc.com 5 个、wired.com 3 个、netlify.com 1 个。看着像写错了,其实不是。MDN 原文:「If the attribute is specified with the defer attribute, the element will act as if only the async attribute is specified.」

这是给那些认识 defer 却不认识 async 的老浏览器留的兜底写法。17 条里多数是第三方标签片段 —— 同意管理、广告交易、Google 的广告标签,这类多年前写下的防御性写法最容易活到现在。BBC 那 5 条里有 3 条是它自家的包,说明这个习惯一旦进了模板就会往里扩散。

10 个写了等于没写的属性

样本里有 9 个内联脚本写了 async,1 个写了 defer。两个都不生效。MDN 在这两个属性下各写了一遍:「This attribute must not be used if the src attribute is absent (i.e., for inline scripts), in this case it would have no effect.」

731 个内联脚本里 10 个,算不上事故。它是一个很稳定的复制粘贴痕迹,值得回自己的模板里 grep 一遍。

一个什么属性都不带的 script 标签也是一个决定。多数时候没人做过这个决定。

一个外链脚本都没有的站

外链脚本数从 0 到 89,中位数 18。最高的是 substack.com 的 89 个,全部带 defer,其中 88 个是模块。另一头是 shopify.com:8 个 script 元素,没有一个带 src,全是内联,所以既没有东西可阻塞,也没有东西要去取。

这两个是从相反方向走到同一个答案。两边都没有阻塞脚本,而且都不是靠一个个补属性补出来的。

这件事对爬虫意味着什么

Google 用无头浏览器渲染,所以阻塞脚本对它花掉的是时间,不是内容。Google 的抓取预算文档,2026-08-26 读到的原文把这两件事连在一起:「Make your pages efficient to load. If Google can load and render your pages faster, we might be able to read more content from your site.」

不渲染的那类检索客户端是另一回事。它们把到手的 HTML 读完就停,阻塞脚本对它们既没有代价也没有收益 —— 那边真正决定成败的是你的正文在不在文档里,这一点在AI 爬虫会不会执行 JavaScript 那篇里讲过。

  • 会碰页面的脚本一律加 defer,顺序还留着
  • 和别的东西互不相干的(比如统计标签)加 async
  • 别不看 script 标签就把第三方片段贴进去。73 条里的 55 条就是这么来的
  • 别给内联脚本加这两个属性。不生效,还暴露了这段是抄来的

head 是这些决定堆积的地方。同一批站上我们还测过27 个首页的 650 条资源提示1,677 张图片的 loading 属性,形状每次都一样:少数几个站是有意安排的,多数站接受了片段给它的默认值。想看页面在这一切跑起来之前是怎么到达引擎的,可以看看 QueryWin 是怎么运转的

常见问题

你们是怎么测的

2026-08-26 每个首页一次请求,浏览器 UA,正文落盘,script 元素用 HTML 解析器读,两个属性按「在不在」判,每个发出来的数字都从落盘文件重算过。

async 和 defer 到底该选哪个

都不是「哪个更快」的问题。脚本需要文档、或者需要排在另一个脚本后面跑,用 defer;两样都不需要,用 async。两个都能让解析器继续往下走,这才是关键。

把脚本放在 body 末尾行不行

行,但它在出现的位置仍然会停住解析器,只是那时候正文已经过去了。放 head 里加 defer 能拿到同样的执行时机,还能更早开始取。我们把 head 里的单独数出来就是为了这件事。

这两个属性会影响 Google 收录吗

不直接影响。Google 会把返回 200 的页面排队去渲染,脚本两种写法都会跑。影响的是页面加载和渲染有多快,而这一点 Google 自己的抓取预算文档把它和「能读到你多少内容」连在了一起。

模块要不要也加 defer

不用。MDN 写得很明确:「The defer attribute has no effect on module scripts — they defer by default.」样本里那 150 个模块本来就不在阻塞这条路上。

async defer 实测:27 个首页 625 个外链脚本,73 个两样都没写