浅谈 script 标签中的 async && defer

发布于 2021-08-04  104 次阅读


script 标签用于加载脚本与执行脚本,在前端开发中可以说是非常重要的标签了。
直接使用 script 脚本的话,html 会按照顺序来加载并执行脚本,在脚本加载&执行的过程中,会阻塞后续的 DOM 渲染。

现在大家习惯于在页面中引用各种的第三方脚本,如果第三方服务商出现了一些小问题,比如延迟之类的,就会使得页面白屏;
好在script提供了两种方式来解决上述问题,asyncdefer,这两个属性使得 script 都不会阻塞 DOM 的渲染;
但既然会存在两个属性,那么就说明,这两个属性之间肯定是有差异的。

1.defer

如果 script 标签设置了该属性,则浏览器会异步的下载该文件并且不会影响到后续 DOM 的渲染
如果有多个设置了 defer 的 script 标签存在,则会按照顺序执行所有的 script;
defer脚本会在文档渲染完毕后,DOMContentLoaded 事件调用前执行。

服务端配置 Server.js

const http = require("http");
const fs   = require("fs");

let respondResourceToClient = (req, res) => {
    fs.createReadStream(req.url.replace(/^\//, "")).pipe(res);
};

let server = http.createServer((req, res) => {
    let extension = req.url.split("/")[req.url.split("/").length - 1];

    let delay = (time) => {
        setTimeout(() => {
            respondResourceToClient(req, res);
        }, time || 0);
    };

    if (extension === "index.html" || extension === "css") {
        delay(0);
    } else if (extension === "1.js") {
        delay(1000);
    } else if (extension === "2.js") {
        delay(2000);
    } else {
        res.end("");
    }
});

server.listen(1314);

console.log("listening at port 1314...");

我们做了一个测试页面,页面中包含了两个 script 标签的加载,给他们都加上 defer 标识。
PS.为了更直观,我们给 1.js 添加了 1s 的延迟,给 2.js 添加了 2s 的延迟。

<html lang="zh-CN">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <meta name="viewport" content="width=device-width,initial-scale=1.0" />
        <title>星宿君</title>
    </head>
    <body>
        <!-- console.log("2.js"); 2s延迟加载 -->
        <script defer type="text/javascript" src="./js/2.js"></script>
        <!-- console.log("1.js"); 1s延迟加载 -->
        <script defer type="text/javascript" src="./js/1.js"></script>
        <script type="text/javascript">
            document.addEventListener("DOMContentLoaded", function() {
                console.log('dom content loaded, ready state:', this.readyState);
            }, false);

            window.addEventListener('load', function() {
                console.log('window loaded, dom ready state:', document.readyState);
            }, false);
        </script>
    </body>
</html>

下图是页面加载的过程 script 脚本的输出顺序;
不难看出,虽然 1.js 加载用时虽然比 2.js 短,但因为 defer 的限制,所以 Ta 只能等前边的脚本执行完毕后才能执行。
file

file

2.async

async 的设置,会使得 script 脚本异步的加载并在允许的情况下执行;
async 的执行,并不会按着 script 在页面中的顺序来执行,而是谁先加载完谁执行。
我们修改测试页面如下:

<!-- console.log("2.js"); 2s延迟加载 -->
<script async type="text/javascript" src="./js/2.js"></script>
<!-- console.log("1.js"); 1s延迟加载 -->
<script async type="text/javascript" src="./js/1.js"></script>

file
遂得到了如下的结果,页面加载时长上,并没有什么变化,毕竟都是异步加载的脚本。
但是我们可以看到一个小细节,DOMContentLoaded事件的触发并不受async脚本加载的影响,在脚本加载完之前,就已经触发了DOMContentLoaded。
file
file
file

其次,如果给async一定的时间,是有可能在DOMContentLoaded事件之前就执行的。

3.对比

普通script

文档解析的过程中,如果遇到 script 脚本,就会停止页面的解析进行下载(但是 Chrome 会做一个优化,如果遇到 script 脚本,会快速的查看后边有没有需要下载其他资源的,如果有的话,会先下载那些资源,然后再进行下载 script 所对应的资源,这样能够节省一部分下载的时间)。
资源的下载是在解析过程中进行的,虽说script1脚本会很快的加载完毕,但是他前边的script2并没有加载&执行,所以他只能处于一个挂起的状态,等待script2执行完毕后再执行。
当这两个脚本都执行完毕后,才会继续解析页面。
file
file

defer

文档解析时,遇到设置了 defer 的脚本,就会在后台进行下载,但是并不会阻止文档的渲染,当页面解析&渲染完毕后。
会等到所有的 defer 脚本加载完毕并按照顺序执行,执行完毕后会触发 DOMContentLoaded 事件。
file

async

async 脚本会在加载完毕后执行;
async 脚本的加载不计入 DOMContentLoaded 事件统计,也就是说下图两种情况都是有可能发生的。
file
file

顺序 DOMContentLoaded 方式
async 加载优先顺序,脚本在文档中的顺序不重要——优先加载完成执行 不相关。可能在文档加载完成前加载并执行完毕。如果脚本很小或者来自于缓存,同时文档足够长,就会发生这种情况。 异步
defer 文档顺序(它们在文档中顺序执行) 在文档加载和解析完成之后(如果需要,则会等待),即在DOMContentLoaded之前执行。 异步

4.推荐的应用场景

defer

  • 如果你的脚本代码依赖于页面中的 DOM 元素(文档是否解析完毕),或者被其他脚本文件依赖。
    例:

    • 评论框
    • 代码语法高亮
    • polyfill.js

async

  • 如果你的脚本并不关心页面中的 DOM 元素(文档是否解析完毕),并且也不会产生其他脚本需要的数据。
    例:

    • 百度统计
    • 如果不太能确定的话,用defer总是会比async稳定。。。

博主好穷啊,快点支助一下吧 ε = = (づ′▽`)づ