Skip to main content

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.jsb.js
  • 下载完 a.jsb.js 仍继续解析 document;
  • 按照页面中出现的顺序,依次执行 a.jsb.js

三、async

async 用于改变处理脚本的行为,在 <script> 标签中设置 async 属性后,浏览器会在遇到脚本时将其立即下载并执行,但不一定按脚本的先后顺序执行。

举个例子:

<script src="a.js" async></script>
<script src="b.js" async></script>

上面代码执行过程如下:

  • 不阻止解析 document, 并行下载 a.jsb.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 属性告诉浏览器异步下载脚本,下载完成后立即执行,不会阻塞页面渲染,也不会按照文档顺序执行。执行顺序是不确定的,哪个脚本先下载完就先执行哪个。由于这种加载和执行行为,在组件中使用异步脚本是一件比较麻烦的事情,因为你不知道它什么时候加载好,不知道它是否重复加载,不方便管理。