阅读 10

async 和 defer

HTML加载过程

1. 两个引擎

浏览器的引擎可以分为渲染引擎和 JS 引擎。 JS 引擎相对独立,而渲染引擎又包括 HTML 解释器、 CSS 解释器、布局、图形、视频、图片解码器等。

JS 引擎独立于渲染引擎,而浏览器加载网页需要 JS 引擎和渲染引擎相互协作。一般情况下,当 HTML 解释器遇到 <script> 标签时,浏览器会将控制权交给 JS 引擎,JS 引擎对内联的代码会直接执行,对外部的 JS 文件需要先下载再执行。当 JS 引擎执行完毕,浏览器又会将控制权交给渲染引擎,继续构建 CSSOM 和 DOM。

但这种写作方式显然有问题,当下载 JS 文件时,会阻塞渲染引擎的工作导致页面加载的延迟,所以才有了 async 和 defer。

HTML文档解析的三种情况

就 JS 同步执行而言,HTML 文档的解析会遇到三种情况:

  • HTML

此时,渲染引擎会直接将 HTML 元素解析成 DOM 树然后进行渲染等操作,在 DOM 树生成的同时触发 DOMContentLoaded 事件。

HTML

  • HTML+CSS

此时,仍然会将 HTML 元素解析成 DOM树,并在 DOM 树生成的同时触发 DOMContentLoaded 事件,但是不同的是,渲染树的生成在有 CSS 参与时,是通过 DOM + CSSOM 共同来渲染的,所以渲染过程的开始是在 DOM 和 CSSOM 都生成完成之后。

HTML+CSS

  • HTML+CSS+JS

同步的情况下,解析 HTML 元素时,如果遇到<script>标签,会有两种操作:获取 JS 代码、执行 JS 代码。如果此时正在生成 CSSOM,那么获取 JS 代码的操作可以和 CSSOM 的构建同时执行,但是只有菜 CSSOM 构建完成之后才能执行 JS 代码。如果<script>标签先于 CSS,那么就不存在等待 CSSOM 的过程。

**获取 JS 代码:**获取操作可以和 CSSOM 的构建同时执行。如果是内联 JS ,则不存在下载的操作,就只有执行操作,如果是外部 JS 文件,那么此时会去下载 JS 文件。 **执行 JS 代码:**需要等待 CSSOM 构建完成之后才可以执行。

HTML+CSS+JS

首屏时间

首屏时间可以理解为 DOMContentLoaded 事件的触发时间。另外,JS 代码写在 HTML 文件的头部和尾部(前)对首屏时间没有影响。其作用是确保 JS 代码执行时, DOM 和 CSSOM树已经解析完毕,在获取元素时不会出现获取为 null 的情况。

普通加载

普通加载就是上文所介绍的同步加载,不再赘述。

写法:

<script src="javascript.js"></script>
复制代码

async

意义:异步加载 JS 文件,加载完成后直接执行。

写法:

<script src="javascript.js" async></script>
复制代码

既然只是异步加载,那么加载完成后可能的结果:

  • HTML 文档解析完毕
  • HTML 文档未解析完毕

其实大部分情况下,async 加载的 JS 脚本会在 HTML 解析完毕之后执行。因为涉及到下载,而 HTML 是直接解析当前的 HTML 文件,所以如果 HTML 和 CSS 中的元素太多太复杂,导致 CSSOM 和 DOM 的解析时间过长,这个时候就会出现 JS 文件下载完毕准备执行时,HTML 文档仍然没有解析完毕。

所以,大部分情况下,不想阻塞 HTML 的解析时,使用 async 就够用了,如果涉及到 JS 脚本的执行顺序或者是为了更加严谨起见,可以使用 defer。

defer

意义:异步加载 JS 文件,全部 JS 文件加载完毕后,且 HTML 文档解析完毕之后,按顺序执行 JS 文件,最后触发 DOMContentLoaded 事件。

写法:

<script src="javascript.js" defer></script>
复制代码

过程如下:

defer

这里有几个点需要注意:

  • 执行 JS 文件是按顺序执行的(特殊情况除外,后文有讲)
  • 执行 JS 文件是在 HTML 文档解析完毕之后,但是实在触发 DOMContentLoaded 事件之前
  • 同样是异步加载 JS 文件,不会阻塞 HTML 文档的解析

验证

验证: HTML 代码如下:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
        <script src="script.js" async></script>

    </head>
    <body>
        <script>
            document.addEventListener("DOMContentLoaded", function() {
                console.log("DOMContentLoaded");
            });
        </script>

        <div>
          <div class="box">盒子1</div>
          此处省略 n 个 div
          <div class="last">盒子2</div>
        </div>
    </body>
</html>
复制代码

JS 代码如下:

console.log("script.js运行")
var lastBox = document.querySelector(".last")
console.log(lastBox)
复制代码

运行情况如下:

  1. 当 div 个数较少时,打印结果总是如下:
DOMContentLoaded
script.js运行
<div class=​"last">​盒子2​</div>​
复制代码
  1. 当 div 个数逐渐增加到临界值时,打印结果:
script.js运行
<div class=​"last">​盒子2​</div>​
DOMContentLoaded
复制代码

或者是:

script.js运行
null
DOMContentLoaded
复制代码
  1. 当 div 超过临界值时,结果总是:
script.js运行
null
DOMContentLoaded
复制代码

当修改成 defer 之后,无论 div 个数为多少,结果总是:

script.js运行
<div class=​"last">​盒子2​</div>​
DOMContentLoaded
复制代码

defer不按顺序执行

有时候即使写了 defer 也不会按照顺序执行,但是多个 JS 文件之前又确实存在先后的依赖关系。此时的解决方案是只写一个 defer ,也就是只包含一个延迟脚本。解决方法又两个,一个是写到一个 JS 文件中,使用 defer 加载这个 JS 文件。另外一个方法就是依赖文件使用同步的方式加载,后执行的 JS 文件使用 defer 异步加载。

如:

<script src="first.js" defer></script>
<script src="seconde.js" defer></script>
复制代码

改成:

<script src="first.js"></script>
<script src="seconde.js" defer></script>
复制代码
关注下面的标签,发现更多相似文章
评论