HTML <script> 脚本的 async 与 defer 属性及不同属性的运行时机与 DOMContentLoaded 事件的关系
浏览器对于带有 async, defer 属性与不携带属性的 <script>
脚本有不同的行为。
它们可以分别翻译为:异步脚本,延迟脚本与同步(阻塞)脚本。
对于模块脚本,默认是 defer 的行为,它也能设置 async,以更改浏览器的处理方式。
同步脚本
不带 async 与 defer 属性的脚本是同步脚本,如果它们出现在文档头部及中间任意位置,会阻塞文档的解析。具体的行为是:
- 文档停止解析(与渲染)
- 请求脚本资源
- 资源下载完毕后立即解析并执行脚本
- 脚本解析完毕后恢复文档的解析
为了更好的用户体验,对于 HTML 文档,浏览器往往是边下载边解析边渲染,而不是等上一项任务完成后再继续下一项。
如果请求脚本资源消耗太长时间,或脚本中有同步的耗时任务,在脚本运行结束前,文档脚本位置之后的内容将不被解析与渲染,出现所谓‘白屏’,这会给人不好的感受。
为了保证文档的正常解析与渲染,如果使用同步脚本,应该将它们放在 <body>
闭合标签之前,这样不管脚本的下载或运行时怎样的慢,至少文档基本的内容是可读的。
关于 DOMContentLoaded 事件,它应该在文档解析完成后触发。但是触发之前,会等待同步脚本与延迟脚本(defer script)运行完毕。
async script (异步脚本)
async script 的下载会另起线程,与文档的解析是并行的,但它下载完成后会立刻解析并运行脚本,不论文档是否解析完毕。此时若文档尚未解析完成,则会阻塞文档的解析,当脚本运行耗时任务,也将会造成‘白屏’。
但是将 async script 放在文档的末尾意义不大,毕竟其下载资源时并不阻塞文档的解析。使用它的目的应该是:
希望脚本尽快执行,并减少对文档解析的阻塞。
由于 async script 会在下载后立即执行,不能预知哪个脚本会先下载完成,所以它们的运行时机是未知、无序的。
async script 可能会在文档解析完成之前或之后下载完成并执行,而 DOMContentLoaded 事件的触发并不会等待 async script 执行完毕,所以并不能知道 async script 会在 DOMContentLoaded 事件触发之前还是之后运行。基于以上原因,不要在 async script 中依赖别的脚本,也不要在 async script 中为 DOMContentLoaded 事件注册回调,回调函数能否被调用是未知的。
基于异步脚本的特点,以下场景适合使用:
-
无依赖,不操作DOM,且需要尽快执行的任务,使用 async script 来减少对文档解析的阻塞。
比如:PV/UV埋点统计
-
在必要的 DOM 加载完成后,尽快为 DOM 元素加载内容、提供交互能力。
由于 HTML 文档往往是边下载边解析边渲染,可以在网页首屏的 DOM 加载完毕后引入 async script,操作DOM元素,使用户可以尽快看到动态加载的内容或与页面互动。并尽量降低对 HTML 文档解析的影响
但是除非文档的数据量很可观,下载与解析要消耗一些时间(3秒以上),否则这么做往往得不偿失。
一般的动态页面,文档体积小,解析起来很快(下载到解析完只要几百毫秒),这么做不但达不到优化目的,阻塞渲染甚至会降低用户体验。
使用 async script 的目的是希望脚本尽快运行,可能是希望提升用户的使用体验,也可能是为了避免用户在脚本运行前就离开页面,而错过什么必要的任务。
使用它很可能会阻塞文档的解析,一定要慎重考量后做决定。
使用 async 也增加了维护成本:我们要记住它运行时机是不确定的,有些操作在该脚本中运行可能会无效,还要忍受它要引入依赖就只能在其前面引入同步脚本。
除此之外,在文档源码中间插入 <script>
标签也并不美观,它不被渲染到视图,还破坏了文档的连贯性。
不管出于什么目的,若决定使用 async script,插入到文档头部或中间,我们一定要保证脚本内的任务快速地运行完毕,不要做耗时操作,将对文档解析的阻塞尽量减少。
defer (延迟脚本)
defer script 会开辟新线程下载脚本,并等待文档解析完毕、所有 defer script 下载完毕后再按脚本在文档中出现的位置依次执行。
DOMContentLoaded 事件,会等待 defer script 全部执行完毕后才触发。
defer script 顺序加载的特点非常适合一个脚本依赖于另一个脚本的情况。
使用 defer script,并将其放到 <head>
中在大多数情况下都是合适的,这么做和将同步脚本放到 </body>
之前的效果一模一样,还能让 <body>
元素保持整洁,毕竟在尾部放一堆不会被渲染的 <script>
看起来不是那么的舒服。