async、defer 和动态脚本
浏览器在读取 HTML 文档,构建 DOM 树的过程中,如果遇到 <script>
标签,渲染引擎会将 DOM 树的构建暂停,先去加载执行 JS 代码,直至脚本执行完毕再继续构建 DOM 树,这就是所谓的渲染阻塞。
因此 <script>
的位置很重要,在实际使用过程中遵循以下两个原则:
- CSS 优先:引入顺序上,CSS 资源先于 JS 资源。
- JS 置后:通常把 JS 代码放到页面底部(
</body>
前),且 JS 应尽量少影响 DOM 的构建。
如果要改变阻塞模式,可以在 <script>
标签上添加 async 或 defer 属性,浏览器会异步的加载和执行 JS 代码,而不会阻塞渲染。
一、普通 script
浏览器遇到脚本时会停止 HTML 渲染,同步获取并执行脚本文件,然后再继续渲染后续的 HTML 内容。
<script src="app.js"></script>

如果直接将 script 放在 body 的最尾部,可以保证 HTML 渲染,同步执行脚本,如下:
<body>
<!-- 其它的 html 内容 -->
<script src="app.js"></script>
</body>

这种做法简单粗暴,也容易实现,所以一些自动化的工具链都采用这种做法。
二、defer
defer 用于改变处理脚本的行为,在 <script>
标签中设置 defer 属性后,浏览器会在遇到脚本时将其立即下载,但延迟到整个页面都解析完毕后再执行。

举个例子:
<script src="a.js" defer></script>
<script src="b.js" defer></script>
上面代码执行过程如下:
- 不阻止解析 document,并行下载
a.js
、b.js; - 下载完
a.js
、b.js 仍继续解析 document; - 按照页面中出现的顺序,依次执行
a.js
、b.js。
三、async
async 用于改变处理脚本的行为,在 <script>
标签中设置 async 属性后,浏览器会在遇到脚本时将其立即下载并执行,但不一定按脚本的先后顺序执行。

举个例子:
<script src="a.js" async></script>
<script src="b.js" async></script>
上面代码执行过程如下:
- 不阻止解析 document, 并行下载
a.js
、b.js; - 当脚本下载完立即执行(不会按照页面中出现的顺序执行)
四、defer 和 async 的区别
defer 和 async 都是异步去加载外部 JS 文件,二者的区别在于脚本下载后何时执行,设置 defer 属性后,浏览器会在遇到脚本时将其立即下载,但延迟到整个页面都解析完毕后再执行,而设置 async 属性后,浏览器会在遇到脚本时将其立即下载并执行,但不一定按脚本的先后顺序执行。
在实际开发中,defer 用于需要整个 DOM 的脚本,或脚本的相对执行顺序很重要的时候,而 async 用于独立脚本,例如计数器或广告,这些脚本的相对执行顺序无关紧要。

但最稳妥的方式还是把 <script>
写在 <body>
底部,这样没有兼容性问题、白屏问题或执行顺序问题。
五、动态脚本
动态脚本即使用 JavaScript 动态地创建一个脚本,并将其添加到文档中。
let script = document.createElement('script');
script.src = "/xx/x.js";
document.body.append(script);
动态脚本的行为是异步的,它们不会等待任何东西,当脚本被添加到文档时就会立即开始加载。
动态脚本按照加载优先的顺序执行(即先加载完成的脚本先执行),如果设置了 script.async = false
,则可以改变这个规则,像 defer 那样按脚本在文档中的先后顺序执行。
举个例子:
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
script.async = false;
document.body.append(script);
}
loadScript("/xx/a.js");
loadScript("/xx/b.js");
上面代码中,a.js
先执行,因为代码中设置了 async = false
,按脚本在文档中的先后顺序执行。
六、总结
- 普通脚本 (
<script src="...">
): 浏览器遇到普通脚本会立即停止解析 HTML,下载并执行脚本,然后继续解析 HTML。这会阻塞页面的渲染。这些脚本会按照它们在 HTML 文档中出现的顺序加载和执行。由于这种阻塞的特性,如果组件在页面靠下的地方才渲染就会很影响体验,很晚才能看到组件出现 - 延迟脚本 (
<script defer src="...">
):defer
属性告诉浏览器在解析完整个 HTML 文档后再按照它们出现的顺序执行脚本。这种方式不会阻塞页面渲染,但仍然需要按照文档顺序加载。如果组件在页面靠下的地方才渲染,依然有白屏时间,但比普通脚本好。 - 异步脚本 (
<script async src="...">
):async
属性告诉浏览器异步下载脚本,下载完成后立即执行,不会阻塞页面渲染,也不会按照文档顺序执行。执行顺序是不确定的,哪个脚本先下载完就先执行哪个。由于这种加载和执行行为,在组件中使用异步脚本是一件比较麻烦的事情,因为你不知道它什么时候加载好,不知道它是否重复加载,不方便管理。