script标签的defer和async

2024-06-12 09:57:19

<script>标签包含了两个特殊的属性:defer和async。

async:属性可选。表示应该立即开始下载脚本,但不能阻止其他页面动作,比如下载资源或等待其他脚本加载。只对外部脚本有效。
defer:属性可选。表示脚本可以延迟到文档完全被解析和显示之后再执行。只对外部脚本文件有效。

如果没有defer和async属性,浏览器会立即加载并执行相应的外部脚本,script标签之下的(下面的、下文的、紧接着的、紧跟着的、后续的)文档元素之前,也就是说不等待载入后续的文档元素,读到脚本就加载并执行这样就阻塞了后续文档的加载

推迟执行脚本defer

HTML4.0.1为<script>标签定义了一个叫defer的属性。这个属性表示脚本在执行的时候不会改变页面的结构。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在<script>标签上设置defer属性,相当于告诉浏览器立即下载,但延迟执行。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js" defer></script>
  <script src="https://unpkg.com/jquery@3.6.4/dist/jquery.min.js" defer></script>
</head>
<body>
  <div>hello world</div>
</body>
</html>

虽然这个例子中的<script>标签包含在页面的<head>中,但它们会在浏览器解析到结束的</html>标签后才会执行。

HTML5规范要求脚本应该按照它们出现的顺序执行,因此第一个推迟的脚本会在第二个推迟的脚本之前执行,而且两者都会在DOMContentLoaded事件之前执行。

不过在实际当中,推迟执行的脚本不一定总会按顺序执行或者在DOMContentLoaded事件之前执行,因此最好只包含一个这样的脚本。

如前所述,defer属性只对外部脚本文件才有效。这是HTML5中明确规定的,因此支持HTML5的浏览器会忽略行内脚本的defer属性。

综述:
1. 无论js文件是否下载完成,只有html解析完毕,才可以执行脚本;
2. 脚本执行的顺序与下载的完成时间无关,按照<script>脚本的位置,顺序执行。

异步执行脚本async

HTML5为<script>标签定义了async属性。从改变脚本处理方式上看,async属性与defer类似。

当然,它们两者也都只适用于外部脚本,都会告诉浏览器立即开始下载。不过,与defer不同的是,标记为async的脚本并不保证能按照它们出现的次序执行

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js" async></script>
  <script src="https://unpkg.com/jquery@3.6.4/dist/jquery.min.js" async></script>
</head>
<body>
  <div>hello world</div>
</body>
</html>

在这个例子中,第二个脚本可能先于第一个脚本执行。因此,重点在于它们之间没有依赖关系。给脚本添加async属性的目的是告诉浏览器,不必等脚本下载和执行完后再加载页面,同样也不必等到该异步脚本下载和执行后再加载其他脚本。正因为如此,异步脚本不应该在加载期间修改DOM

异步脚本保证会在页面的load事件前执行,但可能会在DOMContentLoaded之前或之后

综述:
1. 无论html是否解析完成,立即执行脚本;
2. 无论有使用多少个async加载脚本,只要脚本下载完成,立即执行脚本。与<script>标签的顺序无关。

load和DOMContentLoaded事件

  • load
    MDN的解释:load应该仅用于检测一个完全加载的页面,当一个资源及其依赖资源已完成加载时,将触发load事件。意思是页面的html、css、js、图片等资源都已经加载完之后才会触发load事件。
  • DOMContentLoaded
    MDN的解释:当初始的HTML文档被完全加载和解析完成之后,DOMContentLoaded事件被触发,而无需等待样式表、图像和子框架的完成加载。

中场总结

script标签JS执行顺序是否阻塞解析HTMLDOMContentLoaded回调
依次等待
<script defer>先下载完所有defer再依次执行等待
<script async>先下载完先执行DOM未解析完时阻塞不等待

不同属性的时间流程图

其中蓝色代表js脚本网络加载时间,红色代表js脚本执行时间,绿色代表html解析。

紫色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的,绿色线代表HTML解析。

也就是说async可能乱序执行,而defer是顺序执行,这也就决定了async比较适用于百度分析或者谷歌分析这类不依赖其他脚本的库,且defer在页面加载完成后才执行,可以在脚本中操作DOM。

从图中可以看到一个普通的<script>标签的加载和解析都是同步的,会阻塞DOM的渲染,这也是我们经常会把<script>写在<body>底部的原因之一,为了防止加载资源而导致的长时间的白屏,另一个原因是js可能会进行DOM操作,所以要在DOM全部渲染完后再执行。

最稳妥的办法还是把<script>写在<body>底部,没有兼容性问题,没有白屏问题,没有执行顺序问题,高枕无忧。

附例

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script>
    console.log('aaa', '000')
  </script>
  <script src="./1.js"></script> // console.log('aaa', '111')
  <script src="./2.js" defer></script> // console.log('aaa', '222')
  <script src="./3.js" async></script> // console.log('aaa', '333')
  <script src="./4.js"></script> // console.log('aaa', '444')
  <script>
    console.log('aaa', '555')
    document.addEventListener('DOMContentLoaded', function() {
      console.log('DOMContentLoaded')
    })
  </script>
  <link rel="stylesheet" type="text/css" href="https://emoji-css.afeld.me/emoji.css">
</head>
<body>
  <div>hello world</div>
  <script>
    console.log('aaa', '666')
  </script>
</body>
</html>

执行结果:

表情
Ctrl + Enter