6

浏览器是如何通过 defer 与 async 属性,优化页面加载速度的?

 1 year ago
source link: https://www.fly63.com/article/detial/12223
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

更新日期: 2022-10-28阅读: 252标签: async分享

扫一扫分享

在面试的时候,经常会遇到一道经典的面试题:如何优化网页加载速度?

常规的回答中总会有一条:css 文件放在页面顶部,把 js 文件放在页面底部。

那么,为什么要把 js 文件放在页面的最底部呢?

我们先来看下这段代码

<!DOCTYPE html>
<html lang="zh">
  <head>
    <title>Hi</title>
    <script>
        console.log("Howdy ~");
    </script>
    <script src="https://unpkg.com/vue@3.2.41/dist/vue.global.js"></script>
    <script src="https://unpkg.com/[email protected]/dist/vue-router.global.js"></script>
  </head>
  <body>
    Hello  ~
  </body>
</html>

他的执行顺序是:

  • 在控制台打印:Howdy ~
  • 请求并执行 vue.global.js
  • 请求并执行 vue-router.global.js
  • 在页面中展示:Hello  ~
  • 触发 domContentLoaded 事件
635bd310b0bfa.jpg

浏览器的解析规则是:如果遇到 script 标签,则暂停构建 DOM,转而开始执行 script 标签,如果是外部 script,那么浏览器还需要一直等待其「下载」并「执行」后,再继续解析后面的 HTML。

如果请求并执行「vue.global.js」需要 3 秒,「vue-router.global.js」需要 2 秒,那么页面中的 Hello ~,则至少需要 5 秒以上才会展示出来。

可以看到,script 标签会阻塞浏览器解析 HTML,如果把 script 都放在 head 中,在网络不佳的情况下,就会导致页面长期处于白屏状态。

在很久以前,一般都是将这些外联脚本,放在 body 标签的最后面,确保先解析展示 body 中的内容,然后再一个个请求执行这些外联脚本。

那有没有其他更优雅的解决方案呢?

答案是肯定的,现在 script 标签新增了 2 个属性:defer 和 async,就是为了解决此类问题,提升页面性能的。

script defer

先看一下 MDN 上的解释:

这个布尔属性被设定用来通知浏览器该脚本将在文档完成解析后,触发 DOMContentLoaded 事件前执行。
有 defer 属性的脚本会阻止 DOMContentLoaded 事件,直到脚本被加载并且解析完成。

文档是直接总结了他的特性,我们先看看下面的代码,展开说说细节,加深一下理解。

<!DOCTYPE html>
<html lang="zh">
  <head>
    <title>Hi</title>
    <script>
      console.log("Howdy ~");
    </script>
    <script defer src="https://unpkg.com/[email protected]/dist/vue.global.js"></script>
    <script defer src="https://unpkg.com/[email protected]/dist/vue-router.global.js"></script>
  </head>
  <body>
    Hello  ~
  </body>
</html>

他的执行顺序是:

  • 在控制台打印:Howdy ~
  • 在页面中展示:Hello ~
  • 请求并执行 vue.global.js
  • 请求并执行 vue-router.global.js
  • 触发 DOMContentLoaded 事件
635bd317cb6f6.jpg

如果在 script 标签上设置了 defer 属性,那么在浏览器解析到这里时,会默默的在后台开始下载此脚本,并继续解析后面的 HTML,并不会阻塞解析操作。

等到 HTML 解析完成之后,浏览器会立即执行后台下载的脚本,脚本执行完成之后,才会触发 DOMContentLoaded 事件。

看起来还是蛮好理解的吧?咱们再来讨论 2 个小细节:

Q1: 如果 HTML 解析完成之后,设置了 defer 属性的脚本还没下载完成,会怎样?

A1: 浏览器会等脚本下载完成之后,再执行此脚本,执行完成之后,再触发 DOMContentLoaded 事件。

Q2: 如果有多个设置了 defer 属性的脚本,那浏览器会如何处理?

A2: 浏览器会并行的在后台下载这些脚本,等 HTML 解析完成,并且所有脚本下载完成之后,再按照他们在 HTML 中出现的相对顺序执行,等所有脚本执行完成之后,再触发 DOMContentLoaded 事件。

最佳实践:

建议所有的外联脚本都默认设置此属性,因为他不会阻塞 HTML 解析,可以并行下载 JavaScript 资源,还可以按照他们在 HTML 中的相对顺序执行,确保有依赖关系的脚本运行时,不会缺少依赖。

在 SPA 的应用中,可以考虑把所有的 script 标签加上 defer 属性,并且放到 body 的最后面。在现代浏览器中,可以并行下载提升速度,也可以确保在老浏览器中,不阻塞浏览器解析 HTML,起到降级的作用。

注意:

  • defer 属性仅适用于外部脚本,如果 script 脚本没有 src,则会忽略 defer 特性。
  • defer 属性对模块脚本(script type='module')无效,因为模块脚本就是以 defer 的形式加载的。

script async

按照惯例,先看一下 MDN 上的解释:

对于普通脚本,如果存在 async 属性,那么普通脚本会被并行请求,并尽快解析和执行。
对于模块脚本,如果存在 async 属性,那么脚本及其所有依赖都会在延缓队列中执行,因此它们会被并行请求,并尽快解析和执行。
该属性能够消除解析阻塞的 Javascript。
解析阻塞的 Javascript 会导致浏览器必须加载并且执行脚本,之后才能继续解析。

感觉这段描述的已经蛮清晰了,不过咱们还是先看看下面的代码,展开说说细节,加深一下理解。

<!DOCTYPE html>
<html lang="zh">
  <head>
    <title>Hi</title>
    <script>
      console.log("Howdy ~");
    </script>
    <script async src="https://google-analytics.com/analytics.js"></script>
    <script async src="https://ads.google.cn/ad.js"></script>
  </head>
  <body>
    Hello  ~
  </body>
</html>

他的执行顺序是:

  • 在控制台打印:Howdy ~
  • 并行请求 analytics.js 和 ad.js
  • 在页面中展示:Hello ~
  • 根据网络的实际情况,以下几项会无序执行

    • 执行 analytics.js(下载完后,立即执行)
    • 执行 ad.js(下载完后,立即执行)
    • 触发 DOMContentLoaded 事件(可能在在上面 2 个脚本之前,之间,之后触发)
635bd31f7e3a1.jpg

浏览器在解析到带有 async 属性的 script 标签时,也不会阻塞页面,同样是在后台默默下载此脚本。当他下载完后,浏览器会暂停解析 HTML,立马执行此脚本。

看起来还是蛮好理解的吧?咱们再来讨论 2 个小细节:

Q1: 如果设置了 async 属性的 script 下载完之后,浏览器还没解析完 HTML,会怎样?

A1: 浏览器会暂停解析 HTML,立马执行此脚本,等执行完之后,再继续解析 HTML。

Q2: 如果有多个 async 属性的 script 标签,那等他们下载完成之后,会按照代码顺序执行吗?

A2: 不会。执行顺序是:谁先下载完成,谁先执行。async 的特点是「完全独立」,不依赖其他内容。

最佳实践:

当我们的项目,需要集成其他独立的第三方库时,可以使用此属性,他们不依赖我们,我们也不依赖于他们。
通过设置此属性,让浏览器异步下载并执行他,是个不错的优化方案。

注意:async 特性仅适用于外部脚本,如果 script 脚本没有 src,则会忽略 async 特性。

defer

  • 不阻塞浏览器解析 HTML,等解析完 HTML 之后,才会执行 script。
  • 会并行下载 JavaScript 资源。
  • 会按照 HTML 中的相对顺序执行脚本。
  • 会在脚本下载并执行完成之后,才会触发 DOMContentLoaded 事件。
  • 在脚本执行过程中,一定可以获取到 HTML 中已有的元素。
  • defer 属性对模块脚本无效。
  • 适用于:所有外部脚本(通过 src 引用的 script)。

async

  • 不阻塞浏览器解析 HTML,但是 script 下载完成后,会立即中断浏览器解析 HTML,并执行此 script。
  • 会并行下载 JavaScript 资源。
  • 互相独立,谁先下载完,谁先执行,没有固定的先后顺序,不可控。
  • 由于没有确定的执行时机,所以在脚本里面可能会获取不到 HTML 中已有的元素。
  • DOMContentLoaded 事件和 script 脚本无相关性,无法确定他们的先后顺序。
  • 适用于:独立的第三方脚本。

另外:async 和 defer 之间最大的区别在于它们的执行时机。

One More Thing

你有没有想过,如果一个 script 标签同时设置 defer 和 async,浏览器会如何处理?

先说结论:从表现形式上来说,async 的优先级比 defer 高,也就是如果同时存在这 2 个属性,那么浏览器将会以 async 的特性去加载此脚本。

这主要分 2 种情况:
如果是「普通脚本」,浏览器会优先判断async属性是否存在,如果存在,则以async特性去加载此脚本,如果不存在,再去判断是否存在defer属性。

如果是「模块脚本」,浏览器会判断async属性是否存在:

  • 如果存在,浏览器会并行下载此模块和他的所有依赖模块,等全部下载完成之后,会立刻执行此脚本。
  • 如果不存在,浏览器也会并行下载此模块和他的所有依赖模块,然后等浏览器解析完 HTML 之后,再执行此脚本。
  • 另外需要注意的是:在模块脚本上设置 defer 属性是无效的。

一图胜千言

最后,用一张图概括一下这两个属性的加载模式吧:

635bd32906801.jpg
  • 为什么浏览器在解析到普通的 script 标签时,必须先执行他?
  • 普通的 script 标签会阻塞浏览器解析 HTML,这会导致什么问题?

本文首发于:https://github.com/mrlmx/blogs/issues/4

链接: https://www.fly63.com/article/detial/12223


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK