在网站页面中,通常需要引入外部 JavaScript 资源,然而外部的 JS 资源可能会导致 DOM 解析阻塞,影响页面加载速度。通过异步或者延迟执行 JS,可以在一定程度上减少外部脚本对页面解析的影响。用法是在 script 标签中使用 defer 和 async,那么它们两个有什么区别呢?
从字面上来理解,defer 是延迟,而 async 则是异步(Asynchronous)。
W3C 对 defer 和 async 的解释大致可以概括为:defer 表示脚本会延迟到文档解析完成后再执行;async 则表示脚本下载完成后即可执行,不需要等待整个文档解析完成。更详细的说明可以参考 script 标签。
概念的东西让人很难琢磨,下面是我对使用不同属性的外部脚本进行的一个实验。用 PHP 模拟一个外部 JavaScript 脚本:
<?php
header("Content-Type:text/javascript;charset=utf-8;");
sleep(2);
?>
console.log("This is the Script");
前台页面代码如下:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<script src="./js.php" defer></script>
<!-- 对上面的引入脚本分别进行 defer 和 async 测试 -->
</head>
<body>
This is The Document.
</body>
</html>
下面两张图片分别是使用 defer 和 async 时的时间线:


注意那条蓝色的时间线是 DOMContentLoaded 时间,可以看到使用 defer 时,DOMContentLoaded 事件是在 JS 脚本执行完之后发生的,而使用 async 时,则不一定要等到脚本执行完成。
为什么会这样呢?
我们知道,如果 JavaScript 引入脚本既不使用 defer 也不使用 async,在传统的脚本加载方式下,浏览器在解析到脚本时,通常需要先下载并执行脚本,然后才能继续解析后面的 HTML。
而根据前面的概念,defer 是延迟执行,async 是异步执行。它们之间最大的区别在于:defer 的执行时间与 HTML 文档解析完成有关,而 async 的执行时间是不确定的,只要脚本下载完成,就可能马上执行。
它们的共同点是,脚本下载本身不会像普通同步脚本那样直接阻塞 HTML 的继续解析。
如果一个页面中有多个使用 defer 属性的外部脚本,它们会按照在文档中的顺序依次执行。
defer和async的应用
由于 defer 会在页面解析完成后执行,所以当 JS 脚本运行时,页面的 DOM 已经基本解析完成,这时候可以直接进行 DOM 操作。
例如上面的代码中,如果把 js.php 改成如下代码:
<?php
header("Content-Type:text/javascript;charset=utf-8;");
sleep(2);
?>
document.body.innerHTML="Body Content Has Been changed";
打开前面的页面,会发现页面内容被修改。
使用 defer 时还需要注意,由于它的执行时机是在文档解析完成之后,因此 defer 脚本会参与 DOMContentLoaded 的时序。
例如:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<script>
document.addEventListener("DOMContentLoaded",function(){
console.log("内部脚本,DOMContentLoaded发生!");
},false);
</script>
<script src="./js.js" defer></script>
<!-- 外部脚本内容:console.log("外部脚本, 加载完毕!"); -->
</head>
<body>
This is The Document.
</body>
</html>
可以发现,defer 外部脚本会在 DOMContentLoaded 之前执行。
而对于使用 async 的外部脚本来说,则是在 JavaScript 文件被完全下载之后尽快执行,所以它可能发生在 DOM 解析完成之前,也可能发生在之后。
这里的问题就是,如果异步加载的 JS 在 DOM 准备好之前执行,那么直接操作 DOM 的代码可能无法正常工作;如果脚本在 DOMContentLoaded 之后才执行,那么依赖这个事件的逻辑也可能已经错过事件触发时机。
例如:
document.addEventListener("DOMContentLoaded",function(){
console.log("DOMContentLoaded Event");
},false);如果这个脚本通过 async 异步加载,而且脚本本身加载时间比较长,那么当脚本真正执行时,DOMContentLoaded 事件可能已经发生,因此事件监听函数就不会被调用。
综上,defer 和 async 最大的区别还是执行时机:
defer:脚本异步下载,但等待 HTML 解析完成后按顺序执行。
async:脚本异步下载,下载完成后立即执行,不保证执行顺序。
两者的执行时机不同,也决定了最终的使用场景不同。实际开发中,需要根据脚本是否依赖 DOM、是否依赖其他脚本以及是否有执行顺序要求来选择合适的方式。
评论0
欢迎分享你的看法,也欢迎补充不同的实践经验。