async 和 defer 并非完全不阻塞:async 执行仍中断 HTML 解析,defer 虽延后执行但阻塞 DOMContentLoaded;二者均不适用于内联脚本或 CSS;预加载扫描器仅识别静态标签,动态插入资源失去提前加载优势。

async 和 defer 都不能让 <script></script> 完全不阻塞执行
很多人误以为加了 async 或 defer 就等于“不阻塞”,其实只是把阻塞点挪了位置。浏览器遇到普通 <script src="a.js"></script> 会立刻暂停 HTML 解析,下载、执行完才继续;async 把下载阶段并行化,但执行仍会中断解析;defer 把执行推迟到 DOM 构建完成之后,看起来“不卡”,但它仍会阻塞 DOMContentLoaded 的触发——只要脚本没执行完,这个事件就不发。
常见错误现象:document.getElementById 在 async 脚本里返回 null;多个 async 脚本因下载快慢不同导致执行顺序错乱,依赖失效;defer 脚本里调用 getComputedStyle 可能拿不到样式,因为 CSS 文件还没加载完或未生效。
-
async只对带src的外部脚本有效,内联脚本加了也忽略 -
defer在 IE9– 完全无效,降级为同步加载,必须额外做兼容判断 - 如果页面有多个
defer脚本,它们按 HTML 中的书写顺序执行,但执行时机统一在 DOM 解析完毕后 -
document.write()在async或defer脚本中会直接清空整个文档,必须移除
非关键 CSS 必须用 media="print" 或 rel="preload" 规避渲染阻塞
CSS 是渲染阻塞资源,哪怕它只用于折叠面板或后台管理页,浏览器也会等它加载并解析完才开始绘制首屏。单纯把 <link rel="stylesheet" href="non-critical.css"> 放到 底部也没用——只要在 里,它就参与渲染树构建。
真正有效的做法是欺骗浏览器:用 media="print" 让它初始不参与渲染,再通过 JS 切换回 media="all";或者用 <link rel="preload" as="style" href="non-critical.css"> 提前拉取,再动态插入 <link rel="stylesheet">。
立即学习“前端免费学习笔记(深入)”;
将 Markdown 文章转换为微信公众号兼容的纯 HTML 格式。当用户要求"转成微信格式"、"生成公众号文章 HTML"、"排版到微信草稿箱"、"微信粘贴格式"时触发。输出带内联样式的纯 HTML,微信编辑器可直接渲染,无需额外适配。支持标题、段落、列表、粗体、行内代码、表格、引用块、代码块、配图的完整转换。
-
media="print"方案兼容性好(IE9+),但切换时可能触发一次重排,需注意样式抖动 -
rel="preload"需配合onload事件或requestIdleCallback插入,否则预加载了也不生效 - 不要对
<link rel="stylesheet">直接加async或defer——这两个属性对 CSS 无效,浏览器会忽略 - 若使用
@import引入 CSS,它永远是阻塞的,且无法被 preload,应改用<link>
type="module" 默认行为接近 defer,但模块间依赖不可混用 defer
ES 模块脚本(<script type="module" src="main.mjs"></script>)默认按 defer 语义执行:异步下载、DOM 解析完成后按顺序执行。但它还有两个关键差异:一是自动启用严格模式,二是支持顶层 await,且模块图内所有依赖必须全部加载解析完才执行。
这意味着你不能在一个 defer 脚本里去 import 一个模块,也不能在模块里用 document.write;更危险的是,如果同时存在 <script defer src="a.js"></script> 和 <script type="module" src="b.mjs"></script>,它们的执行时机不一致——前者在 DOM 解析完立即执行,后者还要等模块图解析完成,顺序不可控。
-
type="module"在 Safari 10.1+、Chrome 61+、Firefox 60+ 支持,IE 完全不支持,需搭配nomodule回退 - 模块脚本中的
import()动态导入返回 Promise,可真正控制加载时机,但必须写在函数内,不能出现在顶层作用域 - 避免混合使用
defer和type="module",尤其当它们存在跨脚本依赖时,容易出现ReferenceError - 模块路径必须是完整 URL 或以
/、./、../开头,相对路径不会自动补全 base URL
预加载扫描器(Preload Scanner)不识别动态插入的 <script></script> 或 <link>
浏览器在首次解析 HTML 时会启动一个轻量级的预加载扫描器(Preload Scanner),它不执行 JS,只快速提取 <script src></script>、<link rel="stylesheet">、<img src alt="HTML头部资源加载阻塞(Blocking)的异步非阻塞重构方案" > 等资源 URL 并提前发起请求。但这个扫描器只跑一遍,且只扫描原始 HTML 字符串——用 JS 动态创建并插入的 <script></script> 或 <link> 完全不会被它捕获。
所以 document.createElement('script') + appendChild 这类动态加载方式,虽然不阻塞 HTML 解析,但失去了预加载优势,资源请求总比原生标签晚几百毫秒。这也是为什么 Google Analytics 推荐的异步代码里,会把 ga.src 写死在 createElement 之后,而不是靠后续 JS 设置。
- 预加载扫描器对
<link rel="preload">有特殊优待,即使放在里,只要在首次解析时出现,也会被提前抓取 -
<script async></script>和<script defer></script>都会被预加载扫描器识别,但type="module"在部分旧版 Chrome 中曾被漏扫,建议始终配rel="preload"显式提示 - 服务端渲染(SSR)输出的 HTML 若含动态插入逻辑,需确保关键资源仍通过静态标签声明,否则首屏加载延迟明显
async 就完事。它需要分清资源类型(JS/CSS/字体)、加载时机(首屏/交互后/空闲期)、执行依赖(DOM/样式/其他脚本),再组合使用 defer、preload、media 切换、动态 import() 等手段。最容易被忽略的,是预加载扫描器的局限性和模块脚本与传统脚本的执行时序冲突——这两点在复杂单页应用里往往成为性能瓶颈的根源。


















