HTML <script> 脚本的 async 与 defer 属性及不同属性的运行时机与 DOMContentLoaded 事件的关系

浏览器对于带有 async, defer 属性与不携带属性的 <script> 脚本有不同的行为。
它们可以分别翻译为:异步脚本,延迟脚本与同步(阻塞)脚本。
对于模块脚本,默认是 defer 的行为,它也能设置 async,以更改浏览器的处理方式。

同步脚本

不带 async 与 defer 属性的脚本是同步脚本,如果它们出现在文档头部及中间任意位置,会阻塞文档的解析。具体的行为是:

  1. 文档停止解析(与渲染)
  2. 请求脚本资源
  3. 资源下载完毕后立即解析并执行脚本
  4. 脚本解析完毕后恢复文档的解析

为了更好的用户体验,对于 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> 看起来不是那么的舒服。

posted @ 2024-05-01 01:39  钰琪  阅读(81)  评论(0编辑  收藏  举报